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}