Skip to main content

daaki_imap/
lib.rs

1//! `daaki` IMAP client library.
2//!
3//! An `IMAP4rev1` (RFC 3501) and `IMAP4rev2` (RFC 9051) async client
4//! built on tokio and rustls. Single crate — parser, types, and connection
5//! in one place.
6//!
7//! # Architecture
8//!
9//! ## Driver task
10//!
11//! [`ImapConnection`] is a lightweight handle — it holds an
12//! `mpsc::Sender<DriverCommand>` and a `watch::Receiver` for state
13//! snapshots. A dedicated tokio task (the *driver*) owns the TCP/TLS
14//! stream exclusively. All public methods take `&self`, submit commands
15//! over the channel, and await a result via a `oneshot`. Dropping a
16//! future mid-flight cannot corrupt the stream because no caller-side
17//! future has access to it — the driver completes the in-flight command
18//! and only the result is abandoned. This makes `tokio::select!` and
19//! `tokio::time::timeout` safe to use around any operation.
20//!
21//! ## Consumer trait
22//!
23//! Each IMAP command is executed by a [`Consumer`](connection::dispatch)
24//! implementation. The driver feeds the consumer pre-classified responses
25//! via `on_response`, then calls `finalize` when the tagged response
26//! arrives. Consumers never make routing decisions — they only accumulate
27//! data they are given. Commands that expect `+` continuations (e.g.
28//! AUTHENTICATE) use the separate `ContinuationConsumer` trait with an
29//! additional `on_continuation` method.
30//!
31//! ## Classification truth table
32//!
33//! The function [`classify`](codec::classification::classify) is the
34//! single source of truth for whether an untagged response belongs to
35//! the current command's result or to the asynchronous event queue.
36//! Every row cites the RFC section that defines the routing rule.
37//! The dispatcher calls `classify` before each response and routes
38//! mechanically — consumers have no routing decision to make.
39//!
40//! ## Typed event queue
41//!
42//! Asynchronous server notifications — ALERTs, EXISTS/EXPUNGE changes,
43//! NOTIFY data, BYE — arrive as [`TypedEvent`]s. Poll them with
44//! [`drain_events`](ImapConnection::drain_events) (non-blocking) or
45//! [`next_event`](ImapConnection::next_event) (with timeout). The
46//! driver publishes events via a non-blocking `DriverEventSink` that
47//! can never suspend the driver's select loop.
48//!
49//! ## Wire reader and protocol state
50//!
51//! All wire reads flow through a private `WireReader` in `mod wire`,
52//! which is visible only within the `connection` module. All protocol
53//! state mutations flow through
54//! `ProtocolState::apply_side_effects` in `mod state` — the primary
55//! mutator. The state module's fields are `pub(self)`, so direct
56//! field assignment from outside `mod state` is a compile error.
57//!
58//! ## `MailboxName`
59//!
60//! Every mailbox name in every public type is [`MailboxName`] — a
61//! validated, decoded UTF-8 newtype with no `From<String>` impl. The
62//! only constructors are `new` (public, validating) and `from_decoded`
63//! (`pub(crate)`, for already-parsed wire data in the codec). The
64//! compiler refuses to smuggle wire-form bytes through any public type.
65
66// `fuzzing` cfg is set by cargo-fuzz (nightly) — not known to check-cfg.
67#![cfg_attr(not(fuzzing), allow(unexpected_cfgs))]
68
69pub mod error;
70pub mod types;
71
72mod codec;
73mod connection;
74
75pub use connection::{
76    typed_event::TypedEvent, IdleEvent, ImapConnection, SearchResult, SessionState, TcpKeepalive,
77    TlsMode,
78};
79/// Re-export the canonical `Address` type from `daaki-message` so consumers
80/// can use a single `Address` type across the IMAP, SMTP, and message crates
81/// without manual field-by-field conversion.
82pub use daaki_message::Address;
83pub use error::Error;
84pub use types::{
85    AclEntry, AppendMessage, BinarySection, BodySection, BodyStructure, Capability,
86    ContentDisposition, ContinuationRequest, CopyResult, Envelope, EnvelopeAddress,
87    EsearchResponse, ExpungeResult, FetchAttr, FetchResponse, Flag, GreetingResponse,
88    GreetingStatus, ImapAtom, ListRightsResponse, MailboxAttribute, MailboxFilter, MailboxInfo,
89    MailboxName, MetadataEntry, MetadataResult, MoveResult, NamespaceDescriptor, NamespaceResponse,
90    NotifyEvent, NotifyEventGroup, NotifySetParams, ObjectId, QresyncParams, QuotaResource,
91    QuotaRootResponse, Response, ResponseCode, SearchCriteria, SelectOptions, SelectedMailbox,
92    SequenceSet, SpecialUse, StatusItem, StatusKind, StatusResult, StoreOperation, StoreResult,
93    TaggedResponse, ThreadNode, UidRange, UntaggedResponse, UntaggedStatus, ValidationError,
94};
95
96/// Result type alias for IMAP operations.
97pub type Result<T> = std::result::Result<T, Error>;
98
99/// Fuzz-only entry points. Not part of the public API.
100///
101/// Exposed behind `#[cfg(fuzzing)]` (set automatically by `cargo-fuzz`) so
102/// that out-of-crate fuzz harnesses can reach the `pub(crate)` codec parsers.
103#[allow(unexpected_cfgs)]
104#[cfg(fuzzing)]
105#[doc(hidden)]
106pub mod fuzz {
107    use crate::codec::decode;
108
109    /// Thin wrapper around [`decode::parse_response_utf8`].
110    ///
111    /// Discards nom's remaining-input slice and returns just the parsed
112    /// [`Response`](crate::types::response::Response), or `None` on any
113    /// parse failure.
114    pub fn parse_response_utf8(
115        input: &[u8],
116        utf8_mode: bool,
117    ) -> Option<crate::types::response::Response> {
118        decode::parse_response_utf8(input, utf8_mode)
119            .ok()
120            .map(|(_, r)| r)
121    }
122
123    /// Thin wrapper around [`decode::parse_greeting`].
124    ///
125    /// Discards nom's remaining-input slice and returns just the parsed
126    /// [`Response`](crate::types::response::Response), or `None` on any
127    /// parse failure.
128    pub fn parse_greeting(input: &[u8]) -> Option<crate::types::response::Response> {
129        decode::parse_greeting(input).ok().map(|(_, r)| r)
130    }
131}
132
133/// Consumer-facing README examples must compile against the current public API.
134///
135/// This turns the README into executable doctests so stale helper signatures
136/// and example code are caught during `cargo test --doc`.
137#[cfg(doctest)]
138#[doc = include_str!("../README.md")]
139mod readme_doctests {}
140
141#[cfg(test)]
142mod tests {
143    /// Consumer-facing README claims must stay aligned with the implemented
144    /// extension surface. RFC 5465 NOTIFY is implemented and must be
145    /// advertised in the support table.
146    #[test]
147    fn readme_advertises_notify_support() {
148        let readme = include_str!("../README.md");
149        assert!(
150            readme.contains("NOTIFY"),
151            "README must advertise RFC 5465 NOTIFY now that it is implemented"
152        );
153    }
154}