daaki-imap 0.2.0

An IMAP4rev1/IMAP4rev2 async client library
Documentation
//! `daaki` IMAP client library.
//!
//! An `IMAP4rev1` (RFC 3501) and `IMAP4rev2` (RFC 9051) async client
//! built on tokio and rustls. Single crate — parser, types, and connection
//! in one place.
//!
//! # Architecture
//!
//! ## Driver task
//!
//! [`ImapConnection`] is a lightweight handle — it holds an
//! `mpsc::Sender<DriverCommand>` and a `watch::Receiver` for state
//! snapshots. A dedicated tokio task (the *driver*) owns the TCP/TLS
//! stream exclusively. All public methods take `&self`, submit commands
//! over the channel, and await a result via a `oneshot`. Dropping a
//! future mid-flight cannot corrupt the stream because no caller-side
//! future has access to it — the driver completes the in-flight command
//! and only the result is abandoned. This makes `tokio::select!` and
//! `tokio::time::timeout` safe to use around any operation.
//!
//! ## Consumer trait
//!
//! Each IMAP command is executed by a [`Consumer`](connection::dispatch)
//! implementation. The driver feeds the consumer pre-classified responses
//! via `on_response`, then calls `finalize` when the tagged response
//! arrives. Consumers never make routing decisions — they only accumulate
//! data they are given. Commands that expect `+` continuations (e.g.
//! AUTHENTICATE) use the separate `ContinuationConsumer` trait with an
//! additional `on_continuation` method.
//!
//! ## Classification truth table
//!
//! The function [`classify`](codec::classification::classify) is the
//! single source of truth for whether an untagged response belongs to
//! the current command's result or to the asynchronous event queue.
//! Every row cites the RFC section that defines the routing rule.
//! The dispatcher calls `classify` before each response and routes
//! mechanically — consumers have no routing decision to make.
//!
//! ## Typed event queue
//!
//! Asynchronous server notifications — ALERTs, EXISTS/EXPUNGE changes,
//! NOTIFY data, BYE — arrive as [`TypedEvent`]s. Poll them with
//! [`drain_events`](ImapConnection::drain_events) (non-blocking) or
//! [`next_event`](ImapConnection::next_event) (with timeout). The
//! driver publishes events via a non-blocking `DriverEventSink` that
//! can never suspend the driver's select loop.
//!
//! ## Wire reader and protocol state
//!
//! All wire reads flow through a private `WireReader` in `mod wire`,
//! which is visible only within the `connection` module. All protocol
//! state mutations flow through
//! `ProtocolState::apply_side_effects` in `mod state` — the primary
//! mutator. The state module's fields are `pub(self)`, so direct
//! field assignment from outside `mod state` is a compile error.
//!
//! ## `MailboxName`
//!
//! Every mailbox name in every public type is [`MailboxName`] — a
//! validated, decoded UTF-8 newtype with no `From<String>` impl. The
//! only constructors are `new` (public, validating) and `from_decoded`
//! (`pub(crate)`, for already-parsed wire data in the codec). The
//! compiler refuses to smuggle wire-form bytes through any public type.

// `fuzzing` cfg is set by cargo-fuzz (nightly) — not known to check-cfg.
#![cfg_attr(not(fuzzing), allow(unexpected_cfgs))]

pub mod error;
pub mod types;

mod codec;
mod connection;

pub use connection::{
    typed_event::TypedEvent, IdleEvent, ImapConnection, SearchResult, SessionState, TcpKeepalive,
    TlsMode,
};
/// Re-export the canonical `Address` type from `daaki-message` so consumers
/// can use a single `Address` type across the IMAP, SMTP, and message crates
/// without manual field-by-field conversion.
pub use daaki_message::Address;
pub use error::Error;
pub use types::{
    AclEntry, AppendMessage, BinarySection, BodySection, BodyStructure, Capability,
    ContentDisposition, ContinuationRequest, CopyResult, Envelope, EnvelopeAddress,
    EsearchResponse, ExpungeResult, FetchAttr, FetchResponse, Flag, GreetingResponse,
    GreetingStatus, ImapAtom, ListRightsResponse, MailboxAttribute, MailboxFilter, MailboxInfo,
    MailboxName, MetadataEntry, MetadataResult, MoveResult, NamespaceDescriptor, NamespaceResponse,
    NotifyEvent, NotifyEventGroup, NotifySetParams, ObjectId, QresyncParams, QuotaResource,
    QuotaRootResponse, Response, ResponseCode, SearchCriteria, SelectOptions, SelectedMailbox,
    SequenceSet, SpecialUse, StatusItem, StatusKind, StatusResult, StoreOperation, StoreResult,
    TaggedResponse, ThreadNode, UidRange, UntaggedResponse, UntaggedStatus, ValidationError,
};

/// Result type alias for IMAP operations.
pub type Result<T> = std::result::Result<T, Error>;

/// Fuzz-only entry points. Not part of the public API.
///
/// Exposed behind `#[cfg(fuzzing)]` (set automatically by `cargo-fuzz`) so
/// that out-of-crate fuzz harnesses can reach the `pub(crate)` codec parsers.
#[allow(unexpected_cfgs)]
#[cfg(fuzzing)]
#[doc(hidden)]
pub mod fuzz {
    use crate::codec::decode;

    /// Thin wrapper around [`decode::parse_response_utf8`].
    ///
    /// Discards nom's remaining-input slice and returns just the parsed
    /// [`Response`](crate::types::response::Response), or `None` on any
    /// parse failure.
    pub fn parse_response_utf8(
        input: &[u8],
        utf8_mode: bool,
    ) -> Option<crate::types::response::Response> {
        decode::parse_response_utf8(input, utf8_mode)
            .ok()
            .map(|(_, r)| r)
    }

    /// Thin wrapper around [`decode::parse_greeting`].
    ///
    /// Discards nom's remaining-input slice and returns just the parsed
    /// [`Response`](crate::types::response::Response), or `None` on any
    /// parse failure.
    pub fn parse_greeting(input: &[u8]) -> Option<crate::types::response::Response> {
        decode::parse_greeting(input).ok().map(|(_, r)| r)
    }
}

/// Consumer-facing README examples must compile against the current public API.
///
/// This turns the README into executable doctests so stale helper signatures
/// and example code are caught during `cargo test --doc`.
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
mod readme_doctests {}

#[cfg(test)]
mod tests {
    /// Consumer-facing README claims must stay aligned with the implemented
    /// extension surface. RFC 5465 NOTIFY is implemented and must be
    /// advertised in the support table.
    #[test]
    fn readme_advertises_notify_support() {
        let readme = include_str!("../README.md");
        assert!(
            readme.contains("NOTIFY"),
            "README must advertise RFC 5465 NOTIFY now that it is implemented"
        );
    }
}