Skip to main content

io_imap/
client.rs

1//! Blocking IMAP client wrapping a `Read + Write` stream with a
2//! per-connection [`Fragmentizer`] and one method per coroutine.
3//!
4//! Session state is intentionally not cached: callers retain what
5//! they need (capability list, selected mailbox, ...).
6
7use core::{
8    any::Any,
9    fmt,
10    future::Future,
11    num::{NonZeroU32, NonZeroU64},
12    sync::atomic::{AtomicBool, Ordering},
13    time::Duration,
14};
15
16use alloc::{borrow::Cow, boxed::Box, collections::BTreeMap, string::String, vec, vec::Vec};
17
18use std::{
19    io::{self, Read, Write},
20    sync::{
21        Arc,
22        mpsc::{self, Receiver, RecvTimeoutError, TryRecvError},
23    },
24    thread::{self, JoinHandle},
25};
26
27use imap_codec::{
28    fragmentizer::Fragmentizer,
29    imap_types::{
30        command::SelectParameter,
31        core::{IString, NString, Vec1},
32        extensions::{
33            enable::CapabilityEnable,
34            sort::SortCriterion,
35            thread::{Thread, ThreadingAlgorithm},
36        },
37        fetch::{MacroOrMessageDataItemNames, MessageDataItem},
38        flag::{Flag, StoreType},
39        mailbox::{ListMailbox, Mailbox},
40        response::Capability,
41        search::SearchKey,
42        sequence::SequenceSet,
43        status::{StatusDataItem, StatusDataItemName},
44    },
45};
46#[cfg(feature = "scram")]
47use io_sasl::rfc5802::SaslScramCreds;
48use thiserror::Error;
49
50#[cfg(feature = "scram")]
51use crate::rfc7677::auth_scram_sha_256::*;
52use crate::{
53    coroutine::*,
54    rfc2971::id::*,
55    rfc3501::{
56        append::*, append_stream::*, capability::*, check::*, close::*, copy::*, create::*,
57        delete::*, examine::*, expunge::*, fetch::*, fetch_stream::*, fetch_stream_batch::*,
58        greeting::*, list::*, login::*, logout::*, lsub::*, noop::*, raw::*, rename::*, search::*,
59        select::*, starttls::*, status::*, store::*, subscribe::*, unsubscribe::*,
60    },
61    rfc3691::unselect::*,
62    rfc4315::expunge_uid::*,
63    rfc5161::enable::*,
64    rfc5256::{sort::*, thread::*},
65    rfc6851::r#move::*,
66    rfc7628::auth_oauthbearer::*,
67    sasl::{auth_anonymous::*, auth_login::*, auth_plain::*, auth_xoauth2::*},
68    session::*,
69    watch::*,
70};
71
72#[cfg(any(
73    feature = "rustls-aws",
74    feature = "rustls-ring",
75    feature = "native-tls"
76))]
77mod connect;
78
79/// Failure causes returned by [`ImapClientStd`].
80#[derive(Debug, Error)]
81pub enum ImapClientError {
82    /// The greeting coroutine failed.
83    #[error(transparent)]
84    Greeting(#[from] ImapGreetingGetError),
85    /// The LOGIN coroutine failed.
86    #[error(transparent)]
87    Login(#[from] ImapLoginError),
88    /// The SASL LOGIN coroutine failed.
89    #[error(transparent)]
90    AuthLogin(#[from] ImapAuthLoginError),
91    /// The SASL PLAIN coroutine failed.
92    #[error(transparent)]
93    AuthPlain(#[from] ImapAuthPlainError),
94    /// The SASL ANONYMOUS coroutine failed.
95    #[error(transparent)]
96    AuthAnonymous(#[from] ImapAuthAnonymousError),
97    /// The SASL OAUTHBEARER coroutine failed.
98    #[error(transparent)]
99    AuthOAuthBearer(#[from] ImapAuthOauthbearerError),
100    /// The SASL XOAUTH2 coroutine failed.
101    #[error(transparent)]
102    AuthXOAuth2(#[from] ImapAuthXoauth2Error),
103    /// The SASL SCRAM-SHA-256 coroutine failed.
104    #[cfg(feature = "scram")]
105    #[error(transparent)]
106    AuthScramSha256(#[from] ImapAuthScramSha256Error),
107    /// The session-opening coroutine failed.
108    #[error(transparent)]
109    SessionOpen(#[from] ImapSessionOpenError),
110    /// The LOGOUT coroutine failed.
111    #[error(transparent)]
112    Logout(#[from] ImapLogoutError),
113    /// The CAPABILITY coroutine failed.
114    #[error(transparent)]
115    Capability(#[from] ImapCapabilityGetError),
116    /// The NOOP coroutine failed.
117    #[error(transparent)]
118    Noop(#[from] ImapNoopError),
119    /// The raw-command coroutine failed.
120    #[error(transparent)]
121    Raw(#[from] ImapRawError),
122    /// The ID coroutine failed.
123    #[error(transparent)]
124    ServerId(#[from] ImapServerIdError),
125    /// The ENABLE coroutine failed.
126    #[error(transparent)]
127    ExtensionEnable(#[from] ImapExtensionEnableError),
128    /// The LIST coroutine failed.
129    #[error(transparent)]
130    MailboxList(#[from] ImapMailboxListError),
131    /// The LSUB coroutine failed.
132    #[error(transparent)]
133    MailboxLsub(#[from] ImapMailboxLsubError),
134    /// The STATUS coroutine failed.
135    #[error(transparent)]
136    MailboxStatus(#[from] ImapMailboxStatusError),
137    /// The CREATE coroutine failed.
138    #[error(transparent)]
139    MailboxCreate(#[from] ImapMailboxCreateError),
140    /// The DELETE coroutine failed.
141    #[error(transparent)]
142    MailboxDelete(#[from] ImapMailboxDeleteError),
143    /// The RENAME coroutine failed.
144    #[error(transparent)]
145    MailboxRename(#[from] ImapMailboxRenameError),
146    /// The SUBSCRIBE coroutine failed.
147    #[error(transparent)]
148    MailboxSubscribe(#[from] ImapMailboxSubscribeError),
149    /// The UNSUBSCRIBE coroutine failed.
150    #[error(transparent)]
151    MailboxUnsubscribe(#[from] ImapMailboxUnsubscribeError),
152    /// The SELECT coroutine failed.
153    #[error(transparent)]
154    MailboxSelect(#[from] ImapMailboxSelectError),
155    /// The EXAMINE coroutine failed.
156    #[error(transparent)]
157    MailboxExamine(#[from] ImapMailboxExamineError),
158    /// The mailbox watcher failed.
159    #[error(transparent)]
160    MailboxWatch(#[from] ImapMailboxWatchError),
161    /// The CLOSE coroutine failed.
162    #[error(transparent)]
163    MailboxClose(#[from] ImapMailboxCloseError),
164    /// The UNSELECT coroutine failed.
165    #[error(transparent)]
166    MailboxUnselect(#[from] ImapMailboxUnselectError),
167    /// The CHECK coroutine failed.
168    #[error(transparent)]
169    MailboxCheck(#[from] ImapMailboxCheckError),
170    /// The EXPUNGE coroutine failed.
171    #[error(transparent)]
172    MailboxExpunge(#[from] ImapMailboxExpungeError),
173    /// The UID EXPUNGE coroutine failed.
174    #[error(transparent)]
175    MessageExpungeUid(#[from] ImapMessageExpungeUidError),
176    /// The SORT coroutine failed.
177    #[error(transparent)]
178    MessageSort(#[from] ImapMessageSortError),
179    /// The FETCH coroutine failed.
180    #[error(transparent)]
181    MessageFetch(#[from] ImapMessageFetchError),
182    /// The streaming FETCH coroutine failed.
183    #[error(transparent)]
184    MessageFetchStream(#[from] ImapMessageFetchStreamError),
185    /// The batched streaming FETCH coroutine failed.
186    #[error(transparent)]
187    MessageFetchStreamBatch(#[from] ImapMessageFetchStreamBatchError),
188    /// The SEARCH coroutine failed.
189    #[error(transparent)]
190    MessageSearch(#[from] ImapMessageSearchError),
191    /// The STORE coroutine failed.
192    #[error(transparent)]
193    MessageStore(#[from] ImapMessageStoreError),
194    /// The COPY coroutine failed.
195    #[error(transparent)]
196    MessageCopy(#[from] ImapMessageCopyError),
197    /// The MOVE coroutine failed.
198    #[error(transparent)]
199    MessageMove(#[from] ImapMessageMoveError),
200    /// The buffered APPEND coroutine failed.
201    #[error(transparent)]
202    MessageAppend(#[from] ImapMessageAppendError),
203    /// The streaming APPEND coroutine failed.
204    #[error(transparent)]
205    MessageAppendStream(#[from] ImapMessageAppendStreamError),
206    /// The THREAD coroutine failed.
207    #[error(transparent)]
208    MessageThread(#[from] ImapMessageThreadError),
209    /// Reading from or writing to the stream failed.
210    #[error(transparent)]
211    Io(#[from] io::Error),
212    /// The STARTTLS coroutine failed.
213    #[error(transparent)]
214    StartTls(#[from] ImapStartTlsError),
215    /// Opening the TCP/TLS connection failed.
216    #[cfg(any(
217        feature = "rustls-aws",
218        feature = "rustls-ring",
219        feature = "native-tls"
220    ))]
221    #[error(transparent)]
222    Tls(#[from] anyhow::Error),
223    /// The LOGIN user or password failed imap-types validation.
224    #[error("Invalid IMAP LOGIN credentials")]
225    InvalidLoginCredentials(#[from] imap_codec::imap_types::error::ValidationError),
226    /// QRESYNC was requested but the capability list lacks it.
227    #[error("IMAP server does not advertise QRESYNC capability")]
228    QresyncNotSupported,
229    /// A QRESYNC SELECT was requested with a zero mod-sequence.
230    #[error("Invalid mod-sequence value: 0")]
231    InvalidModSeq,
232    /// The implementor's own transport failed.
233    ///
234    /// [`ImapClientStd`] reports I/O through [`Self::Io`]; this variant
235    /// exists for implementors whose failures are something else, such
236    /// as a JNI upcall or a runtime-specific socket error.
237    #[error(transparent)]
238    Transport(Box<dyn core::error::Error + Send + Sync>),
239}
240
241/// Emits the [`ImapClient`] and [`ImapClientAsync`] command surfaces
242/// from a single list of delegations.
243///
244/// Both traits carry the same forty-odd one-line bodies, differing only
245/// in whether they hand back a value or a future. Writing them twice is
246/// how two implementations of one thing drift apart, which is the defect
247/// this crate is otherwise busy removing, so the list is written once
248/// and expanded twice.
249macro_rules! imap_client_commands {
250    (
251        $(
252            $(#[$meta:meta])*
253            fn $name:ident($($arg:ident: $ty:ty),* $(,)?) -> $out:ty {
254                $coroutine:expr
255            }
256        )*
257    ) => {
258        /// Blocking IMAP command surface: implement [`run`] and inherit
259        /// every command.
260        ///
261        /// [`ImapClientStd`] implements it over a `Read + Write` stream;
262        /// a caller whose transport is its own (a JNI upcall bridge, a
263        /// pre-authenticated proxy socket, an in-memory test double)
264        /// implements the same one method and gets the rest.
265        ///
266        /// The `Yield = ImapYield` bound on [`run`] is deliberate: it
267        /// admits exactly the coroutines every client wraps identically.
268        /// The five that declare their own yield vocabulary (watch,
269        /// idle, streamed APPEND and the two streamed FETCHes) are the
270        /// ones implementations are expected to wire differently, so
271        /// they cannot be defaulted here and are not meant to be. See
272        /// the examples folder for one wiring of each.
273        ///
274        /// The trait is not dyn-compatible, because [`run`] is generic.
275        /// The dynamism this crate needs lives one layer down, at
276        /// [`ImapStream`], which already spans TCP, TLS, unix sockets
277        /// and foreign bridges behind a single concrete client type.
278        ///
279        /// [`run`]: Self::run
280        pub trait ImapClient {
281            /// Runs a standard-shape coroutine to completion, fulfilling
282            /// its read and write requests against the transport.
283            fn run<C, T, E>(&mut self, coroutine: C) -> Result<T, ImapClientError>
284            where
285                C: ImapCoroutine<Yield = ImapYield, Return = Result<T, E>>,
286                ImapClientError: From<E>;
287
288            $(
289                $(#[$meta])*
290                fn $name(&mut self, $($arg: $ty),*) -> Result<$out, ImapClientError> {
291                    self.run($coroutine)
292                }
293            )*
294
295            /// `LOGIN`. Channel must be TLS-protected.
296            fn login(
297                &mut self,
298                user: &str,
299                password: &str,
300                opts: ImapLoginOptions,
301            ) -> Result<Vec<Capability<'static>>, ImapClientError> {
302                self.run(ImapLogin::new(user, password, opts)?)
303            }
304
305            /// Sends one or more caller-tagged command lines
306            /// byte-for-byte and returns the verbatim server response.
307            ///
308            /// The bytes are written exactly as given (no tag is
309            /// injected, no CRLF is trimmed or appended), so callers must
310            /// tag every command and separate them with CRLF. The
311            /// response spans up to and including the tagged completion
312            /// of every command, which may arrive out of order.
313            fn raw(&mut self, command: &[u8]) -> Result<String, ImapClientError> {
314                self.run(ImapRaw::new(command)?)
315            }
316
317            /// `SELECT <mailbox> (QRESYNC ...)`.
318            ///
319            /// Errors with `QresyncNotSupported` when `capability` lacks
320            /// QRESYNC, with `InvalidModSeq` when `highest_mod_seq` is 0.
321            fn select_qresync(
322                &mut self,
323                mailbox: Mailbox<'static>,
324                uid_validity: NonZeroU32,
325                highest_mod_seq: u64,
326                capability: &[Capability<'static>],
327            ) -> Result<ImapMailboxSelectData, ImapClientError> {
328                let parameters = qresync_parameters(uid_validity, highest_mod_seq, capability)?;
329                self.select(mailbox, ImapMailboxSelectOptions { parameters })
330            }
331        }
332
333        /// Async IMAP command surface, the [`ImapClient`] twin for
334        /// callers whose transport is a future.
335        ///
336        /// Everything [`ImapClient`] documents applies here, plus the
337        /// `Send` bounds. They are load-bearing rather than defensive: a
338        /// plain `async fn` in a trait cannot promise that the future it
339        /// returns is `Send`, so anything built from the default bodies
340        /// would fail to compile under `tokio::spawn`, which is the first
341        /// thing a worker-spawning consumer reaches for. Declaring the
342        /// return type explicitly as `impl Future<..> + Send`, with
343        /// `Send` as a supertrait so `&mut Self` carries through, keeps
344        /// the defaults spawnable.
345        ///
346        /// [`ImapClient`] deliberately carries no such bound. A blocking
347        /// call returns a value, so there is no future whose auto-traits
348        /// need pinning down, and requiring `Send` there would exclude a
349        /// perfectly good client built on a thread-affine handle.
350        pub trait ImapClientAsync: Send {
351            /// Runs a standard-shape coroutine to completion, fulfilling
352            /// its read and write requests against the transport.
353            fn run<C, T, E>(
354                &mut self,
355                coroutine: C,
356            ) -> impl Future<Output = Result<T, ImapClientError>> + Send
357            where
358                C: ImapCoroutine<Yield = ImapYield, Return = Result<T, E>> + Send,
359                T: Send,
360                E: Send,
361                ImapClientError: From<E>;
362
363            $(
364                $(#[$meta])*
365                fn $name(
366                    &mut self,
367                    $($arg: $ty),*
368                ) -> impl Future<Output = Result<$out, ImapClientError>> + Send {
369                    self.run($coroutine)
370                }
371            )*
372
373            /// `LOGIN`. Channel must be TLS-protected.
374            fn login(
375                &mut self,
376                user: &str,
377                password: &str,
378                opts: ImapLoginOptions,
379            ) -> impl Future<Output = Result<Vec<Capability<'static>>, ImapClientError>> + Send
380            {
381                async move { self.run(ImapLogin::new(user, password, opts)?).await }
382            }
383
384            /// Sends one or more caller-tagged command lines
385            /// byte-for-byte and returns the verbatim server response.
386            ///
387            /// The bytes are written exactly as given (no tag is
388            /// injected, no CRLF is trimmed or appended), so callers must
389            /// tag every command and separate them with CRLF. The
390            /// response spans up to and including the tagged completion
391            /// of every command, which may arrive out of order.
392            fn raw(
393                &mut self,
394                command: &[u8],
395            ) -> impl Future<Output = Result<String, ImapClientError>> + Send {
396                async move { self.run(ImapRaw::new(command)?).await }
397            }
398
399            /// `SELECT <mailbox> (QRESYNC ...)`.
400            ///
401            /// Errors with `QresyncNotSupported` when `capability` lacks
402            /// QRESYNC, with `InvalidModSeq` when `highest_mod_seq` is 0.
403            fn select_qresync(
404                &mut self,
405                mailbox: Mailbox<'static>,
406                uid_validity: NonZeroU32,
407                highest_mod_seq: u64,
408                capability: &[Capability<'static>],
409            ) -> impl Future<Output = Result<ImapMailboxSelectData, ImapClientError>> + Send
410            {
411                async move {
412                    let parameters =
413                        qresync_parameters(uid_validity, highest_mod_seq, capability)?;
414                    self.select(mailbox, ImapMailboxSelectOptions { parameters })
415                        .await
416                }
417            }
418        }
419    };
420}
421
422imap_client_commands! {
423    /// Consumes the greeting and reports the advertised capabilities
424    /// along with whether the session opened already authenticated.
425    ///
426    /// Forces a CAPABILITY round-trip when the greeting carried none.
427    fn greeting() -> ImapGreetingOk {
428        ImapGreetingGet::new(ImapGreetingGetOptions { ensure_capabilities: true })
429    }
430
431    /// `STARTTLS`. Caller still has to upgrade the socket and refresh
432    /// capabilities.
433    ///
434    /// Returns any bytes pre-read past the tagged response; a non-empty
435    /// return is a STARTTLS-injection signal, refuse the upgrade.
436    /// [`ImapSessionOpen`] does that refusal for you.
437    fn starttls() -> Vec<u8> {
438        ImapStartTls::new()
439    }
440
441    /// SASL `AUTHENTICATE ANONYMOUS`.
442    fn auth_anonymous(
443        message: Option<&str>,
444        opts: ImapAuthAnonymousOptions,
445    ) -> Vec<Capability<'static>> {
446        ImapAuthAnonymous::new(message, opts)
447    }
448
449    /// SASL `AUTHENTICATE LOGIN` (legacy). Prefer auth_plain or
450    /// auth_scram_sha256 when supported.
451    fn auth_login(
452        user: &str,
453        password: &str,
454        opts: ImapAuthLoginOptions,
455    ) -> Vec<Capability<'static>> {
456        ImapAuthLogin::new(user, password, opts)
457    }
458
459    /// SASL `AUTHENTICATE PLAIN`.
460    fn auth_plain(
461        authzid: Option<&str>,
462        authcid: &str,
463        password: &str,
464        opts: ImapAuthPlainOptions,
465    ) -> Vec<Capability<'static>> {
466        ImapAuthPlain::new(authzid, authcid, password, opts)
467    }
468
469    /// SASL `AUTHENTICATE OAUTHBEARER`. Channel must be TLS-protected.
470    fn auth_oauthbearer(
471        user: &str,
472        host: &str,
473        port: u16,
474        token: &str,
475        opts: ImapAuthOauthbearerOptions,
476    ) -> Vec<Capability<'static>> {
477        ImapAuthOauthbearer::new(user, host, port, token, opts)
478    }
479
480    /// SASL `AUTHENTICATE XOAUTH2` (Google's pre-standard mechanism).
481    /// Prefer auth_oauthbearer when supported.
482    fn auth_xoauth2(
483        user: &str,
484        token: &str,
485        opts: ImapAuthXoauth2Options,
486    ) -> Vec<Capability<'static>> {
487        ImapAuthXoauth2::new(user, token, opts)
488    }
489
490    /// SASL `AUTHENTICATE SCRAM-SHA-256`.
491    ///
492    /// The credentials carry the client nonce, which RFC 5802 wants
493    /// drawn from at least 18 bytes of cryptographic randomness, and the
494    /// channel binding deciding whether the exchange announces
495    /// `SCRAM-SHA-256` or `SCRAM-SHA-256-PLUS`.
496    #[cfg(feature = "scram")]
497    fn auth_scram_sha256(
498        creds: SaslScramCreds,
499        opts: ImapAuthScramSha256Options,
500    ) -> Vec<Capability<'static>> {
501        ImapAuthScramSha256::new(creds, opts)
502    }
503
504    /// `LOGOUT`; ends the session.
505    fn logout() -> () {
506        ImapLogout::new()
507    }
508
509    /// `CAPABILITY`; returns the advertised capabilities.
510    fn capability() -> Vec<Capability<'static>> {
511        ImapCapabilityGet::new()
512    }
513
514    /// `NOOP`; round-trips to keep the connection alive or poll for
515    /// updates.
516    fn noop() -> () {
517        ImapNoop::new()
518    }
519
520    /// `ID`. An `opts.parameters` of `None` sends `ID NIL`.
521    fn id(
522        opts: ImapServerIdOptions,
523    ) -> Option<Vec<(IString<'static>, NString<'static>)>> {
524        ImapServerId::new(opts)
525    }
526
527    /// `ENABLE`; returns the capabilities the server confirmed enabling.
528    fn enable(
529        capabilities: Vec1<CapabilityEnable<'static>>,
530    ) -> Option<Vec<CapabilityEnable<'static>>> {
531        ImapExtensionEnable::new(capabilities)
532    }
533
534    /// `LIST`; returns the mailboxes matching `reference` and `pattern`.
535    fn list(
536        reference: Mailbox<'static>,
537        pattern: ListMailbox<'static>,
538    ) -> ImapMailboxListing {
539        ImapMailboxList::new(reference, pattern)
540    }
541
542    /// `LSUB`; returns the subscribed mailboxes matching `reference` and
543    /// `pattern`.
544    fn lsub(
545        reference: Mailbox<'static>,
546        pattern: ListMailbox<'static>,
547    ) -> ImapMailboxListing {
548        ImapMailboxLsub::new(reference, pattern)
549    }
550
551    /// `STATUS`; returns the requested status items for `mailbox`.
552    fn status(
553        mailbox: Mailbox<'static>,
554        item_names: Cow<'static, [StatusDataItemName]>,
555    ) -> Vec<StatusDataItem> {
556        ImapMailboxStatus::new(mailbox, item_names)
557    }
558
559    /// `CREATE`; creates `mailbox`.
560    fn create(mailbox: Mailbox<'static>) -> () {
561        ImapMailboxCreate::new(mailbox)
562    }
563
564    /// `DELETE`; deletes `mailbox`.
565    fn delete(mailbox: Mailbox<'static>) -> () {
566        ImapMailboxDelete::new(mailbox)
567    }
568
569    /// `RENAME`; renames mailbox `from` to `to`.
570    fn rename(from: Mailbox<'static>, to: Mailbox<'static>) -> () {
571        ImapMailboxRename::new(from, to)
572    }
573
574    /// `SUBSCRIBE`; subscribes to `mailbox`.
575    fn subscribe(mailbox: Mailbox<'static>) -> () {
576        ImapMailboxSubscribe::new(mailbox)
577    }
578
579    /// `UNSUBSCRIBE`; unsubscribes from `mailbox`.
580    fn unsubscribe(mailbox: Mailbox<'static>) -> () {
581        ImapMailboxUnsubscribe::new(mailbox)
582    }
583
584    /// `SELECT`; opens `mailbox` for read-write and returns its state.
585    fn select(
586        mailbox: Mailbox<'static>,
587        opts: ImapMailboxSelectOptions,
588    ) -> ImapMailboxSelectData {
589        ImapMailboxSelect::new(mailbox, opts)
590    }
591
592    /// `EXAMINE`; opens `mailbox` read-only and returns its state.
593    fn examine(
594        mailbox: Mailbox<'static>,
595        opts: ImapMailboxExamineOptions,
596    ) -> ImapMailboxSelectData {
597        ImapMailboxExamine::new(mailbox, opts)
598    }
599
600    /// `CLOSE`; expunges deleted messages and unselects the mailbox.
601    fn close() -> () {
602        ImapMailboxClose::new()
603    }
604
605    /// `UNSELECT`; unselects the mailbox without expunging.
606    fn unselect() -> () {
607        ImapMailboxUnselect::new()
608    }
609
610    /// `CHECK`; requests a mailbox checkpoint.
611    fn check() -> () {
612        ImapMailboxCheck::new()
613    }
614
615    /// `EXPUNGE`; returns the expunged sequence numbers.
616    fn expunge() -> Vec<NonZeroU32> {
617        ImapMailboxExpunge::new()
618    }
619
620    /// `UID EXPUNGE <sequence_set>` (RFC 4315); permanently removes only
621    /// the `\Deleted` messages whose UID is in `sequence_set`, leaving
622    /// any other `\Deleted` message untouched.
623    ///
624    /// Requires the server to advertise `UIDPLUS`; returns the expunged
625    /// sequence numbers.
626    fn uid_expunge(sequence_set: SequenceSet) -> Vec<NonZeroU32> {
627        ImapMessageExpungeUid::new(sequence_set)
628    }
629
630    /// `FETCH`; returns the requested items keyed by message id.
631    fn fetch(
632        sequence_set: SequenceSet,
633        items: MacroOrMessageDataItemNames<'static>,
634        opts: ImapMessageFetchOptions,
635    ) -> BTreeMap<NonZeroU32, Vec1<MessageDataItem<'static>>> {
636        ImapMessageFetch::new(sequence_set, items, opts)
637    }
638
639    /// `SEARCH`; returns the ids matching `criteria`.
640    fn search(
641        criteria: Vec1<SearchKey<'static>>,
642        opts: ImapMessageSearchOptions,
643    ) -> Vec<NonZeroU32> {
644        ImapMessageSearch::new(criteria, opts)
645    }
646
647    /// `STORE` (echo variant); returns the server-reported FETCH echoes.
648    fn store(
649        sequence_set: SequenceSet,
650        kind: StoreType,
651        flags: Vec<Flag<'static>>,
652        opts: ImapMessageStoreOptions,
653    ) -> BTreeMap<NonZeroU32, Vec1<MessageDataItem<'static>>> {
654        ImapMessageStore::new(sequence_set, kind, flags, opts)
655    }
656
657    /// `COPY`; copies messages to `mailbox` and returns the optional
658    /// COPYUID pair.
659    fn copy(
660        sequence_set: SequenceSet,
661        mailbox: Mailbox<'static>,
662        opts: ImapMessageCopyOptions,
663    ) -> ImapCopyUid {
664        ImapMessageCopy::new(sequence_set, mailbox, opts)
665    }
666
667    /// `MOVE`; moves messages to `mailbox` and returns the optional
668    /// COPYUID pair.
669    fn r#move(
670        sequence_set: SequenceSet,
671        mailbox: Mailbox<'static>,
672        opts: ImapMessageMoveOptions,
673    ) -> ImapCopyUid {
674        ImapMessageMove::new(sequence_set, mailbox, opts)
675    }
676
677    /// `APPEND`; returns the optional EXISTS count and APPENDUID pair.
678    ///
679    /// Buffered: the whole `message` is held in memory. For large
680    /// messages prefer the streaming APPEND coroutine.
681    fn append(
682        mailbox: Mailbox<'static>,
683        message: &[u8],
684        opts: ImapMessageAppendOptions,
685    ) -> ImapMessageAppendOutput {
686        ImapMessageAppend::new(mailbox, message.to_vec(), opts)
687    }
688
689    /// `SORT` with a client-side fallback.
690    ///
691    /// With `opts.fallback == false` this is a plain server SORT; with
692    /// `opts.fallback == true` it SEARCHes, FETCHes the sort keys, and
693    /// sorts locally. Feed `fallback` from a SORT capability check (the
694    /// server SORT requires the extension).
695    fn sort(
696        sort_criteria: Vec1<SortCriterion>,
697        search_criteria: Vec1<SearchKey<'static>>,
698        opts: ImapMessageSortOptions,
699    ) -> Vec<NonZeroU32> {
700        ImapMessageSort::new(sort_criteria, search_criteria, opts)
701    }
702
703    /// `THREAD`; returns the message threads matching `search_criteria`.
704    fn thread(
705        algorithm: ThreadingAlgorithm<'static>,
706        search_criteria: Vec1<SearchKey<'static>>,
707        opts: ImapMessageThreadOptions,
708    ) -> Vec<Thread> {
709        ImapMessageThread::new(algorithm, search_criteria, opts)
710    }
711}
712
713/// Validates a QRESYNC SELECT against the advertised capabilities,
714/// shared by both traits' `select_qresync`.
715fn qresync_parameters(
716    uid_validity: NonZeroU32,
717    highest_mod_seq: u64,
718    capability: &[Capability<'static>],
719) -> Result<Vec<SelectParameter>, ImapClientError> {
720    if !capability.contains(&Capability::QResync) {
721        return Err(ImapClientError::QresyncNotSupported);
722    }
723
724    let Some(mod_sequence_value) = NonZeroU64::new(highest_mod_seq) else {
725        return Err(ImapClientError::InvalidModSeq);
726    };
727
728    Ok(vec![SelectParameter::QResync {
729        uid_validity,
730        mod_sequence_value,
731        known_uids: None,
732        seq_match_data: None,
733    }])
734}
735
736const READ_BUFFER_SIZE: usize = 16 * 1024;
737/// Buffer for streaming a message body from the socket into the caller's sink.
738/// Larger than [`READ_BUFFER_SIZE`] because a body transfer is bulk data, not
739/// line-oriented parsing: 128 KB cuts the `read`/`write` syscall count (and TLS
740/// record crossings) on a large body versus the 8 KB `io::copy` default, for a
741/// small `sys`-time win. Heap-allocated once per fetch and reused.
742const BODY_COPY_BUFFER_SIZE: usize = 128 * 1024;
743const FRAGMENTIZER_MAX_MESSAGE_SIZE: u32 = 100 * 1024 * 1024;
744
745// NOTE: both are protocol constants rather than client state, so they
746// live next to the scheme table in the session module and stay reachable
747// without the client feature. Re-exported here because config layers
748// already reach for them through this path.
749pub use crate::session::{default_alpn, default_port};
750
751/// Blocking IMAP client: a stream, the connection-wide `Fragmentizer`
752/// and one method per coroutine.
753pub struct ImapClientStd {
754    /// The stream carrying the connection to the IMAP server.
755    pub stream: Box<dyn ImapStream>,
756    /// The connection-wide parser buffer shared by every coroutine run
757    /// on this connection.
758    pub fragmentizer: Fragmentizer,
759    /// ID parameters consumed by every auth_*/login call; required by
760    /// a few providers (mail.qq.com, fastmail).
761    ///
762    /// `None` skips, `Some(empty)` sends `ID NIL`, `Some(params)`
763    /// sends `ID (k v ...)`.
764    pub auto_id: Option<Vec<(IString<'static>, NString<'static>)>>,
765    /// Whether the server greeting was `PREAUTH`: the session opened
766    /// already authenticated (a socket proxy such as sirup), so
767    /// [`connect`](Self::connect) skipped the SASL step. Stays `false`
768    /// on a freshly-opened connection.
769    pub pre_authenticated: bool,
770}
771
772impl ImapClient for ImapClientStd {
773    fn run<C, T, E>(&mut self, mut coroutine: C) -> Result<T, ImapClientError>
774    where
775        C: ImapCoroutine<Yield = ImapYield, Return = Result<T, E>>,
776        ImapClientError: From<E>,
777    {
778        let mut buf = [0u8; READ_BUFFER_SIZE];
779        let mut arg: Option<&[u8]> = None;
780
781        loop {
782            match coroutine.resume(&mut self.fragmentizer, arg.take()) {
783                ImapCoroutineState::Complete(Ok(out)) => return Ok(out),
784                ImapCoroutineState::Complete(Err(err)) => return Err(err.into()),
785                ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
786                    let n = self.read_response(&mut buf)?;
787                    arg = Some(&buf[..n]);
788                }
789                ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
790                    self.stream.write_all(&bytes)?;
791                    arg = None;
792                }
793            }
794        }
795    }
796}
797
798impl ImapClientStd {
799    /// Caller is responsible for opening the connection (TCP, TLS,
800    /// STARTTLS).
801    pub fn new<S: ImapStream + 'static>(stream: S) -> Self {
802        Self {
803            stream: Box::new(stream),
804            fragmentizer: Fragmentizer::new(FRAGMENTIZER_MAX_MESSAGE_SIZE),
805            auto_id: None,
806            pre_authenticated: false,
807        }
808    }
809
810    /// Reads the next bytes of a server response, a closed connection
811    /// being an `UnexpectedEof` failure rather than an empty read.
812    ///
813    /// Mid-response is the wrong place for the peer to hang up: the
814    /// coroutine waiting on those bytes would otherwise be resumed with
815    /// an empty slice forever. Not-ready failures never reach here, the
816    /// stream retrying them under its own strategy.
817    fn read_response(&mut self, buf: &mut [u8]) -> Result<usize, ImapClientError> {
818        match self.stream.read(buf)? {
819            0 => {
820                let kind = io::ErrorKind::UnexpectedEof;
821                let err = io::Error::new(kind, "IMAP server closed the connection");
822                Err(err.into())
823            }
824            n => Ok(n),
825        }
826    }
827
828    /// Useful after a STARTTLS upgrade or on reconnection.
829    pub fn set_stream<S: ImapStream + 'static>(&mut self, stream: S) {
830        self.stream = Box::new(stream);
831    }
832
833    /// Consumes the client into a background watcher.
834    ///
835    /// Drop the returned stream (or call its `close`) to wind down.
836    /// `capability` selects the QRESYNC path or the whole-mailbox
837    /// fallback, and `opts.shutdown_poll` how long winding down may
838    /// take.
839    pub fn watch_mailbox(
840        self,
841        mailbox: Mailbox<'static>,
842        capability: &[Capability<'static>],
843        opts: ImapMailboxWatchStreamOptions,
844    ) -> Result<ImapMailboxWatchStream, ImapClientError> {
845        let shutdown = Arc::new(AtomicBool::new(false));
846        let watch_opts = ImapMailboxWatchOptions {
847            idle_timeout: opts.idle_timeout,
848            poll: opts.poll.is_some(),
849        };
850        let mut watcher = ImapMailboxWatch::new(capability, mailbox, shutdown.clone(), watch_opts);
851        let mut fragmentizer = self.fragmentizer;
852        let mut stream = self.stream;
853
854        stream.set_read_timeout(Some(opts.shutdown_poll))?;
855        stream.stop_retrying();
856
857        let (tx, rx) = mpsc::sync_channel::<Result<ImapMailboxWatchEvent, ImapClientError>>(256);
858        let shutdown_handle = shutdown.clone();
859        let handle = thread::spawn(move || {
860            let mut buf = [0u8; READ_BUFFER_SIZE];
861            let mut arg: Option<Vec<u8>> = None;
862
863            loop {
864                match watcher.resume(&mut fragmentizer, arg.as_deref()) {
865                    ImapCoroutineState::Yielded(ImapMailboxWatchYield::Event(e)) => {
866                        arg = None;
867                        if tx.send(Ok(e)).is_err() {
868                            return;
869                        }
870                    }
871                    ImapCoroutineState::Complete(Ok(())) => return,
872                    ImapCoroutineState::Yielded(ImapMailboxWatchYield::WantsRead) => {
873                        match stream.read(&mut buf) {
874                            Ok(0) => {
875                                let eof = io::ErrorKind::UnexpectedEof;
876                                let err = "IMAP server closed the connection during watch";
877                                tx.send(Err(io::Error::new(eof, err).into())).ok();
878                                return;
879                            }
880                            Ok(n) => arg = Some(buf[..n].to_vec()),
881                            // NOTE: the SO_RCVTIMEO wakeup this loop arms
882                            // on purpose, which is why retries were turned
883                            // off above: here a not-ready stream is not a
884                            // failure but the periodic chance to re-check
885                            // shutdown and otherwise resume, so the
886                            // coroutine can observe the flag and issue
887                            // IDLE DONE.
888                            Err(err)
889                                if matches!(
890                                    err.kind(),
891                                    io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut
892                                ) =>
893                            {
894                                if shutdown.load(Ordering::SeqCst) {
895                                    return;
896                                }
897                                arg = None;
898                            }
899                            Err(err) => {
900                                tx.send(Err(err.into())).ok();
901                                return;
902                            }
903                        }
904                    }
905                    ImapCoroutineState::Yielded(ImapMailboxWatchYield::WantsWrite(bytes)) => {
906                        if let Err(err) = stream.write_all(&bytes) {
907                            tx.send(Err(err.into())).ok();
908                            return;
909                        }
910                        arg = None;
911                    }
912                    // NOTE: only a polling watch asks to wait, and it
913                    // is slept here in shutdown-poll steps so closing
914                    // the stream does not have to outwait the whole
915                    // interval.
916                    ImapCoroutineState::Yielded(ImapMailboxWatchYield::WantsWait) => {
917                        let mut left = opts.poll.unwrap_or_default();
918
919                        while left > Duration::ZERO {
920                            if shutdown.load(Ordering::SeqCst) {
921                                return;
922                            }
923
924                            let step = left.min(opts.shutdown_poll);
925                            thread::sleep(step);
926                            left -= step;
927                        }
928
929                        arg = None;
930                    }
931                    ImapCoroutineState::Complete(Err(err)) => {
932                        tx.send(Err(err.into())).ok();
933                        return;
934                    }
935                }
936            }
937        });
938
939        Ok(ImapMailboxWatchStream {
940            rx,
941            handle: Some(handle),
942            shutdown: shutdown_handle,
943        })
944    }
945
946    /// `FETCH <id> (BODY.PEEK[])` streaming the message body straight
947    /// into `sink`; the body never lands in memory whole.
948    ///
949    /// Peek leaves `\Seen` untouched. Returns once the tagged response
950    /// is parsed; a missing id completes with an empty sink.
951    pub fn fetch_body_stream(
952        &mut self,
953        id: NonZeroU32,
954        uid: bool,
955        mut sink: impl Write,
956    ) -> Result<(), ImapClientError> {
957        let mut coroutine = ImapMessageFetchStream::new(id, uid);
958        let mut buf = [0u8; READ_BUFFER_SIZE];
959        let mut body_buf = vec![0u8; BODY_COPY_BUFFER_SIZE];
960        let mut arg: Option<&[u8]> = None;
961
962        loop {
963            match coroutine.resume(&mut self.fragmentizer, arg.take()) {
964                ImapCoroutineState::Complete(Ok(())) => return Ok(()),
965                ImapCoroutineState::Complete(Err(err)) => return Err(err.into()),
966                ImapCoroutineState::Yielded(ImapMessageFetchStreamYield::WantsRead) => {
967                    let n = self.read_response(&mut buf)?;
968                    arg = Some(&buf[..n]);
969                }
970                ImapCoroutineState::Yielded(ImapMessageFetchStreamYield::WantsWrite(bytes)) => {
971                    self.stream.write_all(&bytes)?;
972                    arg = None;
973                }
974                ImapCoroutineState::Yielded(ImapMessageFetchStreamYield::BodyChunk(bytes)) => {
975                    sink.write_all(&bytes)?;
976                    arg = None;
977                }
978                ImapCoroutineState::Yielded(ImapMessageFetchStreamYield::WantsStream { len }) => {
979                    let mut remaining = len as u64;
980                    let mut short = false;
981
982                    while remaining > 0 {
983                        let want = remaining.min(body_buf.len() as u64) as usize;
984                        let n = self.stream.read(&mut body_buf[..want])?;
985                        if n == 0 {
986                            short = true;
987                            break;
988                        }
989                        sink.write_all(&body_buf[..n])?;
990                        remaining -= n as u64;
991                    }
992
993                    arg = short.then_some(&[]);
994                }
995            }
996        }
997    }
998
999    /// `UID FETCH <set> (UID BODY.PEEK[])` streaming every message body in one
1000    /// command — N bodies for one round trip. Each message is routed to its own
1001    /// sink: `open(uid)` returns a fresh sink when a message begins, its body is
1002    /// streamed into it, and `done(uid, sink)` commits it when the message ends.
1003    /// No body is held in memory whole. A requested UID absent on the server
1004    /// simply never calls `open`/`done`.
1005    pub fn fetch_bodies_stream<S: Write>(
1006        &mut self,
1007        sequence_set: SequenceSet,
1008        uid: bool,
1009        mut open: impl FnMut(u32) -> io::Result<S>,
1010        mut done: impl FnMut(u32, S) -> io::Result<()>,
1011    ) -> Result<(), ImapClientError> {
1012        let mut coroutine = ImapMessageFetchStreamBatch::new(sequence_set, uid);
1013        let mut buf = [0u8; READ_BUFFER_SIZE];
1014        let mut body_buf = vec![0u8; BODY_COPY_BUFFER_SIZE];
1015        // The sink of the message currently streaming, opened at MessageStart and
1016        // committed at MessageEnd.
1017        let mut current: Option<(u32, S)> = None;
1018        let mut arg: Option<&[u8]> = None;
1019
1020        loop {
1021            match coroutine.resume(&mut self.fragmentizer, arg.take()) {
1022                ImapCoroutineState::Complete(Ok(())) => return Ok(()),
1023                ImapCoroutineState::Complete(Err(err)) => return Err(err.into()),
1024                ImapCoroutineState::Yielded(ImapMessageFetchStreamBatchYield::WantsRead) => {
1025                    let n = self.read_response(&mut buf)?;
1026                    arg = Some(&buf[..n]);
1027                }
1028                ImapCoroutineState::Yielded(ImapMessageFetchStreamBatchYield::WantsWrite(
1029                    bytes,
1030                )) => {
1031                    self.stream.write_all(&bytes)?;
1032                    arg = None;
1033                }
1034                ImapCoroutineState::Yielded(ImapMessageFetchStreamBatchYield::MessageStart {
1035                    uid,
1036                }) => {
1037                    current = Some((uid, open(uid)?));
1038                    arg = None;
1039                }
1040                ImapCoroutineState::Yielded(ImapMessageFetchStreamBatchYield::BodyChunk(bytes)) => {
1041                    let (_, sink) = current.as_mut().expect("body chunk within a message");
1042                    sink.write_all(&bytes)?;
1043                    arg = None;
1044                }
1045                ImapCoroutineState::Yielded(ImapMessageFetchStreamBatchYield::WantsStream {
1046                    len,
1047                }) => {
1048                    let (_, sink) = current.as_mut().expect("stream within a message");
1049                    let mut remaining = len as u64;
1050                    let mut short = false;
1051                    while remaining > 0 {
1052                        let want = remaining.min(body_buf.len() as u64) as usize;
1053                        let n = self.stream.read(&mut body_buf[..want])?;
1054                        if n == 0 {
1055                            short = true;
1056                            break;
1057                        }
1058                        sink.write_all(&body_buf[..n])?;
1059                        remaining -= n as u64;
1060                    }
1061                    arg = short.then_some(&[]);
1062                }
1063                ImapCoroutineState::Yielded(ImapMessageFetchStreamBatchYield::MessageEnd) => {
1064                    let (uid, sink) = current.take().expect("message end within a message");
1065                    done(uid, sink)?;
1066                    arg = None;
1067                }
1068            }
1069        }
1070    }
1071
1072    /// `APPEND` streaming `len` octets from `source` straight to the
1073    /// socket; the body never lands in memory whole.
1074    ///
1075    /// `len` must match the source exactly: IMAP declares the octet
1076    /// count up front, so a shorter source poisons the connection.
1077    /// Synchronising by default so the server can reject before the
1078    /// body is sent; set `opts.non_sync` to skip the wait.
1079    pub fn append_stream(
1080        &mut self,
1081        mailbox: Mailbox<'static>,
1082        mut source: impl Read,
1083        len: usize,
1084        opts: ImapMessageAppendOptions,
1085    ) -> Result<ImapMessageAppendOutput, ImapClientError> {
1086        let mut coroutine = ImapMessageAppendStream::new(mailbox, len as u32, opts);
1087        let mut buf = [0u8; READ_BUFFER_SIZE];
1088        let mut arg: Option<&[u8]> = None;
1089
1090        loop {
1091            match coroutine.resume(&mut self.fragmentizer, arg.take()) {
1092                ImapCoroutineState::Complete(Ok(out)) => return Ok(out),
1093                ImapCoroutineState::Complete(Err(err)) => return Err(err.into()),
1094                ImapCoroutineState::Yielded(ImapMessageAppendStreamYield::WantsRead) => {
1095                    let n = self.read_response(&mut buf)?;
1096                    arg = Some(&buf[..n]);
1097                }
1098                ImapCoroutineState::Yielded(ImapMessageAppendStreamYield::WantsWrite(bytes)) => {
1099                    self.stream.write_all(&bytes)?;
1100                    arg = None;
1101                }
1102                ImapCoroutineState::Yielded(ImapMessageAppendStreamYield::WantsStream) => {
1103                    let len = len as u64;
1104                    let mut sink = source.by_ref().take(len);
1105                    let n = io::copy(&mut sink, &mut self.stream)?;
1106                    arg = (n != len).then_some(&[]);
1107                }
1108            }
1109        }
1110    }
1111}
1112
1113impl fmt::Debug for ImapClientStd {
1114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1115        f.debug_struct("ImapClientStd")
1116            .field("fragmentizer", &self.fragmentizer)
1117            .finish_non_exhaustive()
1118    }
1119}
1120
1121/// Options of [`ImapClientStd::watch_mailbox`].
1122#[derive(Clone, Copy, Debug)]
1123pub struct ImapMailboxWatchStreamOptions {
1124    /// How long the worker may sit in a read before it looks at the
1125    /// shutdown flag again.
1126    ///
1127    /// It is the read deadline armed on the stream, so it is also the
1128    /// worst case for [`ImapMailboxWatchStream::close`] against a
1129    /// silent server: the worker is blocked in a read until then. A
1130    /// caller that wants a prompt Ctrl+C picks a second or less and
1131    /// pays a wakeup per interval; the default of five seconds suits a
1132    /// long-running watch nobody is waiting on.
1133    pub shutdown_poll: Duration,
1134    /// How long an IDLE is held before it is re-issued, `None` taking
1135    /// io-imap's own default. Ignored when `poll` selects the polling
1136    /// watch, which holds no IDLE.
1137    pub idle_timeout: Option<Duration>,
1138    /// Re-read the mailbox on this interval instead of holding IDLE.
1139    ///
1140    /// `None`, the default, holds IDLE and lets the server speak
1141    /// first, which is what a server offering it deserves. A value
1142    /// picks the polling watch and sets how long it waits between two
1143    /// re-reads, for a server whose IDLE cannot be trusted.
1144    pub poll: Option<Duration>,
1145}
1146
1147impl Default for ImapMailboxWatchStreamOptions {
1148    fn default() -> Self {
1149        Self {
1150            shutdown_poll: Duration::from_secs(5),
1151            idle_timeout: None,
1152            poll: None,
1153        }
1154    }
1155}
1156
1157/// Background-worker watch stream; drop or [`Self::close`] to wind down.
1158pub struct ImapMailboxWatchStream {
1159    rx: Receiver<Result<ImapMailboxWatchEvent, ImapClientError>>,
1160    handle: Option<JoinHandle<()>>,
1161    shutdown: Arc<AtomicBool>,
1162}
1163
1164impl ImapMailboxWatchStream {
1165    /// Non-blocking probe for the next event.
1166    pub fn try_recv(&self) -> Result<Result<ImapMailboxWatchEvent, ImapClientError>, TryRecvError> {
1167        self.rx.try_recv()
1168    }
1169
1170    /// Waits up to `timeout` for the next event.
1171    pub fn recv_timeout(
1172        &self,
1173        timeout: Duration,
1174    ) -> Result<Result<ImapMailboxWatchEvent, ImapClientError>, RecvTimeoutError> {
1175        self.rx.recv_timeout(timeout)
1176    }
1177
1178    /// Signals shutdown and joins the worker.
1179    pub fn close(mut self) -> Result<(), ImapClientError> {
1180        self.shutdown.store(true, Ordering::SeqCst);
1181        if let Some(handle) = self.handle.take() {
1182            handle
1183                .join()
1184                .map_err(|_| io::Error::other("IMAP watch worker panicked"))?;
1185        }
1186        Ok(())
1187    }
1188}
1189
1190impl Iterator for ImapMailboxWatchStream {
1191    type Item = Result<ImapMailboxWatchEvent, ImapClientError>;
1192
1193    fn next(&mut self) -> Option<Self::Item> {
1194        self.rx.recv().ok()
1195    }
1196}
1197
1198impl Drop for ImapMailboxWatchStream {
1199    fn drop(&mut self) {
1200        self.shutdown.store(true, Ordering::SeqCst);
1201
1202        if let Some(handle) = self.handle.take() {
1203            handle.join().ok();
1204        }
1205    }
1206}
1207
1208/// Blocking stream the client runs over.
1209///
1210/// Implemented for the standard pimalaya-stream transport; a custom one (such
1211/// as a JNI upcall bridge) implements it directly. `as_any_mut`
1212/// supports downcasting back to the concrete stream when a caller needs
1213/// a type-specific handle (e.g. sirup's socket proxy).
1214pub trait ImapStream: Read + Write + Send + Any {
1215    /// The stream as a mutable `Any`, ready for downcasting.
1216    fn as_any_mut(&mut self) -> &mut dyn Any;
1217
1218    /// Bounds each blocking read, used by the mailbox watch worker for a
1219    /// periodic shutdown-poll wakeup. A transport that cannot honor it
1220    /// returns `Ok(())` and manages its own read semantics.
1221    fn set_read_timeout(&self, timeout: Option<Duration>) -> io::Result<()>;
1222
1223    /// Hands the failures that only mean "not ready yet" back to the
1224    /// caller instead of retrying them, for good.
1225    ///
1226    /// The mailbox watch worker is what needs it: it arms a read
1227    /// timeout precisely to be woken up, and a stream retrying the
1228    /// wakeup away would leave the shutdown flag unchecked until the
1229    /// server speaks. A transport that never retries has nothing to
1230    /// turn off.
1231    fn stop_retrying(&mut self) {}
1232}