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