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::*, greeting::*, list::*,
90        login::*, logout::*, lsub::*, noop::*, raw::*, rename::*, search::*, select::*,
91        starttls::*, status::*, store::*, subscribe::*, unsubscribe::*,
92    },
93    rfc3691::unselect::*,
94    rfc5161::enable::*,
95    rfc5256::{sort::*, thread::*},
96    rfc6851::r#move::*,
97    rfc7628::auth_oauthbearer::*,
98    sasl::{auth_anonymous::*, auth_login::*, auth_plain::*, auth_xoauth2::*},
99    watch::*,
100};
101
102/// Failure causes returned by [`ImapClientStd`].
103#[derive(Debug, Error)]
104pub enum ImapClientStdError {
105    /// The greeting coroutine failed.
106    #[error(transparent)]
107    Greeting(#[from] ImapGreetingGetError),
108    /// The LOGIN coroutine failed.
109    #[error(transparent)]
110    Login(#[from] ImapLoginError),
111    /// The SASL LOGIN coroutine failed.
112    #[error(transparent)]
113    AuthLogin(#[from] ImapAuthLoginError),
114    /// The SASL PLAIN coroutine failed.
115    #[error(transparent)]
116    AuthPlain(#[from] ImapAuthPlainError),
117    /// The SASL ANONYMOUS coroutine failed.
118    #[error(transparent)]
119    AuthAnonymous(#[from] ImapAuthAnonymousError),
120    /// The SASL OAUTHBEARER coroutine failed.
121    #[error(transparent)]
122    AuthOAuthBearer(#[from] ImapAuthOauthbearerError),
123    /// The SASL XOAUTH2 coroutine failed.
124    #[error(transparent)]
125    AuthXOAuth2(#[from] ImapAuthXoauth2Error),
126    /// The SASL SCRAM-SHA-256 coroutine failed.
127    #[cfg(feature = "scram")]
128    #[error(transparent)]
129    AuthScramSha256(#[from] ImapAuthScramSha256Error),
130    /// SCRAM-SHA-256 was requested but the scram feature is off.
131    #[cfg(any(
132        feature = "rustls-aws",
133        feature = "rustls-ring",
134        feature = "native-tls"
135    ))]
136    #[cfg(not(feature = "scram"))]
137    #[error("SCRAM-SHA-256 SASL mechanism requires the `scram` cargo feature")]
138    ScramSha256NotEnabled,
139    /// The LOGOUT coroutine failed.
140    #[error(transparent)]
141    Logout(#[from] ImapLogoutError),
142    /// The CAPABILITY coroutine failed.
143    #[error(transparent)]
144    Capability(#[from] ImapCapabilityGetError),
145    /// The NOOP coroutine failed.
146    #[error(transparent)]
147    Noop(#[from] ImapNoopError),
148    /// The raw-command coroutine failed.
149    #[error(transparent)]
150    Raw(#[from] ImapRawError),
151    /// The ID coroutine failed.
152    #[error(transparent)]
153    ServerId(#[from] ImapServerIdError),
154    /// The ENABLE coroutine failed.
155    #[error(transparent)]
156    ExtensionEnable(#[from] ImapExtensionEnableError),
157    /// The LIST coroutine failed.
158    #[error(transparent)]
159    MailboxList(#[from] ImapMailboxListError),
160    /// The LSUB coroutine failed.
161    #[error(transparent)]
162    MailboxLsub(#[from] ImapMailboxLsubError),
163    /// The STATUS coroutine failed.
164    #[error(transparent)]
165    MailboxStatus(#[from] ImapMailboxStatusError),
166    /// The CREATE coroutine failed.
167    #[error(transparent)]
168    MailboxCreate(#[from] ImapMailboxCreateError),
169    /// The DELETE coroutine failed.
170    #[error(transparent)]
171    MailboxDelete(#[from] ImapMailboxDeleteError),
172    /// The RENAME coroutine failed.
173    #[error(transparent)]
174    MailboxRename(#[from] ImapMailboxRenameError),
175    /// The SUBSCRIBE coroutine failed.
176    #[error(transparent)]
177    MailboxSubscribe(#[from] ImapMailboxSubscribeError),
178    /// The UNSUBSCRIBE coroutine failed.
179    #[error(transparent)]
180    MailboxUnsubscribe(#[from] ImapMailboxUnsubscribeError),
181    /// The SELECT coroutine failed.
182    #[error(transparent)]
183    MailboxSelect(#[from] ImapMailboxSelectError),
184    /// The EXAMINE coroutine failed.
185    #[error(transparent)]
186    MailboxExamine(#[from] ImapMailboxExamineError),
187    /// The mailbox watcher failed.
188    #[error(transparent)]
189    MailboxWatch(#[from] ImapMailboxWatchError),
190    /// The CLOSE coroutine failed.
191    #[error(transparent)]
192    MailboxClose(#[from] ImapMailboxCloseError),
193    /// The UNSELECT coroutine failed.
194    #[error(transparent)]
195    MailboxUnselect(#[from] ImapMailboxUnselectError),
196    /// The CHECK coroutine failed.
197    #[error(transparent)]
198    MailboxCheck(#[from] ImapMailboxCheckError),
199    /// The EXPUNGE coroutine failed.
200    #[error(transparent)]
201    MailboxExpunge(#[from] ImapMailboxExpungeError),
202    /// The SORT coroutine failed.
203    #[error(transparent)]
204    MessageSort(#[from] ImapMessageSortError),
205    /// The FETCH coroutine failed.
206    #[error(transparent)]
207    MessageFetch(#[from] ImapMessageFetchError),
208    /// The streaming FETCH coroutine failed.
209    #[error(transparent)]
210    MessageFetchStream(#[from] ImapMessageFetchStreamError),
211    /// The SEARCH coroutine failed.
212    #[error(transparent)]
213    MessageSearch(#[from] ImapMessageSearchError),
214    /// The STORE coroutine failed.
215    #[error(transparent)]
216    MessageStore(#[from] ImapMessageStoreError),
217    /// The COPY coroutine failed.
218    #[error(transparent)]
219    MessageCopy(#[from] ImapMessageCopyError),
220    /// The MOVE coroutine failed.
221    #[error(transparent)]
222    MessageMove(#[from] ImapMessageMoveError),
223    /// The buffered APPEND coroutine failed.
224    #[error(transparent)]
225    MessageAppend(#[from] ImapMessageAppendError),
226    /// The streaming APPEND coroutine failed.
227    #[error(transparent)]
228    MessageAppendStream(#[from] ImapMessageAppendStreamError),
229    /// The THREAD coroutine failed.
230    #[error(transparent)]
231    MessageThread(#[from] ImapMessageThreadError),
232    /// Reading from or writing to the stream failed.
233    #[error(transparent)]
234    Io(#[from] io::Error),
235    /// The STARTTLS coroutine failed.
236    #[error(transparent)]
237    StartTls(#[from] ImapStartTlsError),
238    /// Opening the TCP/TLS connection failed.
239    #[cfg(any(
240        feature = "rustls-aws",
241        feature = "rustls-ring",
242        feature = "native-tls"
243    ))]
244    #[error(transparent)]
245    Tls(#[from] anyhow::Error),
246    /// The connect URL has no host to connect to.
247    #[cfg(any(
248        feature = "rustls-aws",
249        feature = "rustls-ring",
250        feature = "native-tls"
251    ))]
252    #[error("IMAP URL `{0}` has no host")]
253    UrlMissingHost(String),
254    /// The connect URL scheme is neither imap nor imaps.
255    #[cfg(any(
256        feature = "rustls-aws",
257        feature = "rustls-ring",
258        feature = "native-tls"
259    ))]
260    #[error("IMAP URL `{0}` has unsupported scheme `{1}` (expected `imap` or `imaps`)")]
261    UrlUnsupportedScheme(String, String),
262    /// STARTTLS was requested on an already-TLS imaps connection.
263    #[cfg(any(
264        feature = "rustls-aws",
265        feature = "rustls-ring",
266        feature = "native-tls"
267    ))]
268    #[error("STARTTLS requested on an `imaps://` URL: TLS is already active")]
269    StartTlsOverTls,
270    /// The LOGIN user or password failed imap-types validation.
271    #[error("Invalid IMAP LOGIN credentials")]
272    InvalidLoginCredentials(#[from] imap_codec::imap_types::error::ValidationError),
273    /// QRESYNC was requested but the capability list lacks it.
274    #[error("IMAP server does not advertise QRESYNC capability")]
275    QresyncNotSupported,
276    /// A QRESYNC SELECT was requested with a zero mod-sequence.
277    #[error("Invalid mod-sequence value: 0")]
278    InvalidModSeq,
279}
280
281const READ_BUFFER_SIZE: usize = 16 * 1024;
282const FRAGMENTIZER_MAX_MESSAGE_SIZE: u32 = 100 * 1024 * 1024;
283
284/// Default ALPN identifier for IMAP TLS (RFC 7595).
285pub fn default_alpn() -> Vec<String> {
286    vec![String::from("imap")]
287}
288
289/// Blocking IMAP client: a stream, the connection-wide `Fragmentizer`
290/// and one method per coroutine.
291pub struct ImapClientStd {
292    /// The stream carrying the connection to the IMAP server.
293    pub stream: Box<dyn ImapStream>,
294    /// The connection-wide parser buffer shared by every coroutine run
295    /// on this connection.
296    pub fragmentizer: Fragmentizer,
297    /// ID parameters consumed by every auth_*/login call; required by
298    /// a few providers (mail.qq.com, fastmail).
299    ///
300    /// `None` skips, `Some(empty)` sends `ID NIL`, `Some(params)`
301    /// sends `ID (k v ...)`.
302    pub auto_id: Option<Vec<(IString<'static>, NString<'static>)>>,
303}
304
305impl ImapClientStd {
306    /// Caller is responsible for opening the connection (TCP, TLS,
307    /// STARTTLS).
308    pub fn new<S: Read + Write + Send + 'static>(stream: S) -> Self {
309        Self {
310            stream: Box::new(stream),
311            fragmentizer: Fragmentizer::new(FRAGMENTIZER_MAX_MESSAGE_SIZE),
312            auto_id: None,
313        }
314    }
315
316    /// Useful after a STARTTLS upgrade or on reconnection.
317    pub fn set_stream<S: Read + Write + Send + 'static>(&mut self, stream: S) {
318        self.stream = Box::new(stream);
319    }
320
321    /// Runs a standard-shape coroutine to completion, fulfilling its
322    /// read and write requests.
323    ///
324    /// Richer yields (IDLE events, watch deltas, streamed bodies) need
325    /// their own per-method loops.
326    pub fn run<C, T, E>(&mut self, mut coroutine: C) -> Result<T, ImapClientStdError>
327    where
328        C: ImapCoroutine<Yield = ImapYield, Return = Result<T, E>>,
329        ImapClientStdError: From<E>,
330    {
331        let mut buf = [0u8; READ_BUFFER_SIZE];
332        let mut arg: Option<&[u8]> = None;
333
334        loop {
335            match coroutine.resume(&mut self.fragmentizer, arg.take()) {
336                ImapCoroutineState::Complete(Ok(out)) => return Ok(out),
337                ImapCoroutineState::Complete(Err(err)) => return Err(err.into()),
338                ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
339                    let n = self.stream.read(&mut buf)?;
340                    arg = Some(&buf[..n]);
341                }
342                ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
343                    self.stream.write_all(&bytes)?;
344                    arg = None;
345                }
346            }
347        }
348    }
349
350    /// Consumes the greeting and returns the advertised capabilities
351    /// (forcing a CAPABILITY round-trip if the greeting carried none).
352    pub fn greeting(&mut self) -> Result<Vec<Capability<'static>>, ImapClientStdError> {
353        Ok(self
354            .run(ImapGreetingGet::new(ImapGreetingGetOptions {
355                ensure_capabilities: true,
356            }))?
357            .capability)
358    }
359
360    /// `LOGIN`. Channel must be TLS-protected. Consumes `auto_id`.
361    pub fn login(
362        &mut self,
363        user: impl AsRef<str>,
364        password: impl AsRef<str>,
365        opts: ImapLoginOptions,
366    ) -> Result<Vec<Capability<'static>>, ImapClientStdError> {
367        self.run(ImapLogin::new(user, password, opts)?)
368    }
369
370    /// `STARTTLS`. Caller still has to upgrade the socket and refresh
371    /// capabilities.
372    ///
373    /// Returns any bytes pre-read past the tagged response; a
374    /// non-empty return is a STARTTLS-injection signal, refuse the
375    /// upgrade.
376    pub fn starttls(&mut self) -> Result<Vec<u8>, ImapClientStdError> {
377        self.run(ImapStartTls::new())
378    }
379
380    /// SASL `AUTHENTICATE ANONYMOUS`. Consumes `auto_id`.
381    pub fn auth_anonymous(
382        &mut self,
383        message: Option<impl AsRef<str>>,
384        opts: ImapAuthAnonymousOptions,
385    ) -> Result<Vec<Capability<'static>>, ImapClientStdError> {
386        self.run(ImapAuthAnonymous::new(message, opts))
387    }
388
389    /// SASL `AUTHENTICATE LOGIN` (legacy). Prefer auth_plain or
390    /// auth_scram_sha256 when supported. Consumes `auto_id`.
391    pub fn auth_login(
392        &mut self,
393        user: impl AsRef<str>,
394        password: impl AsRef<str>,
395        opts: ImapAuthLoginOptions,
396    ) -> Result<Vec<Capability<'static>>, ImapClientStdError> {
397        self.run(ImapAuthLogin::new(user, password, opts))
398    }
399
400    /// SASL `AUTHENTICATE PLAIN`. Consumes `auto_id`.
401    pub fn auth_plain(
402        &mut self,
403        authzid: Option<impl AsRef<str>>,
404        authcid: impl AsRef<str>,
405        password: impl AsRef<str>,
406        opts: ImapAuthPlainOptions,
407    ) -> Result<Vec<Capability<'static>>, ImapClientStdError> {
408        self.run(ImapAuthPlain::new(authzid, authcid, password, opts))
409    }
410
411    /// SASL `AUTHENTICATE OAUTHBEARER`. Channel must be
412    /// TLS-protected. Consumes `auto_id`.
413    pub fn auth_oauthbearer(
414        &mut self,
415        user: impl AsRef<str>,
416        host: impl AsRef<str>,
417        port: u16,
418        token: impl AsRef<str>,
419        opts: ImapAuthOauthbearerOptions,
420    ) -> Result<Vec<Capability<'static>>, ImapClientStdError> {
421        self.run(ImapAuthOauthbearer::new(user, host, port, token, opts))
422    }
423
424    /// SASL `AUTHENTICATE XOAUTH2` (Google's pre-standard mechanism).
425    /// Prefer auth_oauthbearer when supported. Consumes `auto_id`.
426    pub fn auth_xoauth2(
427        &mut self,
428        user: impl AsRef<str>,
429        token: impl AsRef<str>,
430        opts: ImapAuthXoauth2Options,
431    ) -> Result<Vec<Capability<'static>>, ImapClientStdError> {
432        self.run(ImapAuthXoauth2::new(user, token, opts))
433    }
434
435    /// SASL `AUTHENTICATE SCRAM-SHA-256`. Consumes `auto_id`.
436    #[cfg(feature = "scram")]
437    pub fn auth_scram_sha256(
438        &mut self,
439        user: impl AsRef<str>,
440        password: impl AsRef<str>,
441        opts: ImapAuthScramSha256Options,
442    ) -> Result<Vec<Capability<'static>>, ImapClientStdError> {
443        self.run(ImapAuthScramSha256::new(user, password, opts))
444    }
445
446    /// `LOGOUT`; ends the session.
447    pub fn logout(&mut self) -> Result<(), ImapClientStdError> {
448        self.run(ImapLogout::new())
449    }
450
451    /// `CAPABILITY`; returns the advertised capabilities.
452    pub fn capability(&mut self) -> Result<Vec<Capability<'static>>, ImapClientStdError> {
453        self.run(ImapCapabilityGet::new())
454    }
455
456    /// `NOOP`; round-trips to keep the connection alive or poll for updates.
457    pub fn noop(&mut self) -> Result<(), ImapClientStdError> {
458        self.run(ImapNoop::new())
459    }
460
461    /// Sends an arbitrary raw command line (no tag, no trailing CRLF)
462    /// and returns the verbatim server response.
463    ///
464    /// The response spans up to and including the tagged completion
465    /// line. Synchronizing literals are not supported.
466    pub fn raw(&mut self, command: impl AsRef<str>) -> Result<String, ImapClientStdError> {
467        self.run(ImapRaw::new(command))
468    }
469
470    /// `ID`. An `opts.parameters` of `None` sends `ID NIL`.
471    pub fn id(
472        &mut self,
473        opts: ImapServerIdOptions,
474    ) -> Result<Option<Vec<(IString<'static>, NString<'static>)>>, ImapClientStdError> {
475        self.run(ImapServerId::new(opts))
476    }
477
478    /// `ENABLE`; returns the capabilities the server confirmed enabling.
479    pub fn enable(
480        &mut self,
481        capabilities: Vec1<CapabilityEnable<'static>>,
482    ) -> Result<Option<Vec<CapabilityEnable<'static>>>, ImapClientStdError> {
483        self.run(ImapExtensionEnable::new(capabilities))
484    }
485
486    /// `LIST`; returns the mailboxes matching `reference` and `pattern`.
487    pub fn list(
488        &mut self,
489        reference: Mailbox<'static>,
490        pattern: ListMailbox<'static>,
491    ) -> Result<ImapMailboxListing, ImapClientStdError> {
492        self.run(ImapMailboxList::new(reference, pattern))
493    }
494
495    /// `LSUB`; returns the subscribed mailboxes matching `reference` and
496    /// `pattern`.
497    pub fn lsub(
498        &mut self,
499        reference: Mailbox<'static>,
500        pattern: ListMailbox<'static>,
501    ) -> Result<ImapMailboxListing, ImapClientStdError> {
502        self.run(ImapMailboxLsub::new(reference, pattern))
503    }
504
505    /// `STATUS`; returns the requested status items for `mailbox`.
506    pub fn status(
507        &mut self,
508        mailbox: Mailbox<'static>,
509        item_names: impl Into<Cow<'static, [StatusDataItemName]>>,
510    ) -> Result<Vec<StatusDataItem>, ImapClientStdError> {
511        self.run(ImapMailboxStatus::new(mailbox, item_names))
512    }
513
514    /// `CREATE`; creates `mailbox`.
515    pub fn create(&mut self, mailbox: Mailbox<'static>) -> Result<(), ImapClientStdError> {
516        self.run(ImapMailboxCreate::new(mailbox))
517    }
518
519    /// `DELETE`; deletes `mailbox`.
520    pub fn delete(&mut self, mailbox: Mailbox<'static>) -> Result<(), ImapClientStdError> {
521        self.run(ImapMailboxDelete::new(mailbox))
522    }
523
524    /// `RENAME`; renames mailbox `from` to `to`.
525    pub fn rename(
526        &mut self,
527        from: Mailbox<'static>,
528        to: Mailbox<'static>,
529    ) -> Result<(), ImapClientStdError> {
530        self.run(ImapMailboxRename::new(from, to))
531    }
532
533    /// `SUBSCRIBE`; subscribes to `mailbox`.
534    pub fn subscribe(&mut self, mailbox: Mailbox<'static>) -> Result<(), ImapClientStdError> {
535        self.run(ImapMailboxSubscribe::new(mailbox))
536    }
537
538    /// `UNSUBSCRIBE`; unsubscribes from `mailbox`.
539    pub fn unsubscribe(&mut self, mailbox: Mailbox<'static>) -> Result<(), ImapClientStdError> {
540        self.run(ImapMailboxUnsubscribe::new(mailbox))
541    }
542
543    /// `SELECT`; opens `mailbox` for read-write and returns its state.
544    pub fn select(
545        &mut self,
546        mailbox: Mailbox<'static>,
547        opts: ImapMailboxSelectOptions,
548    ) -> Result<ImapMailboxSelectData, ImapClientStdError> {
549        self.run(ImapMailboxSelect::new(mailbox, opts))
550    }
551
552    /// `EXAMINE`; opens `mailbox` read-only and returns its state.
553    pub fn examine(
554        &mut self,
555        mailbox: Mailbox<'static>,
556        opts: ImapMailboxExamineOptions,
557    ) -> Result<ImapMailboxSelectData, ImapClientStdError> {
558        self.run(ImapMailboxExamine::new(mailbox, opts))
559    }
560
561    /// `SELECT <mailbox> (QRESYNC ...)`.
562    ///
563    /// Errors with `QresyncNotSupported` when `capability` lacks
564    /// QRESYNC, with `InvalidModSeq` when `highest_mod_seq` is 0.
565    pub fn select_qresync(
566        &mut self,
567        mailbox: Mailbox<'static>,
568        uid_validity: NonZeroU32,
569        highest_mod_seq: u64,
570        capability: &[Capability<'static>],
571    ) -> Result<ImapMailboxSelectData, ImapClientStdError> {
572        if !capability.contains(&Capability::QResync) {
573            return Err(ImapClientStdError::QresyncNotSupported);
574        }
575
576        let Some(highest_mod_seq) = NonZeroU64::new(highest_mod_seq) else {
577            return Err(ImapClientStdError::InvalidModSeq);
578        };
579
580        let parameters = vec![SelectParameter::QResync {
581            uid_validity,
582            mod_sequence_value: highest_mod_seq,
583            known_uids: None,
584            seq_match_data: None,
585        }];
586
587        self.select(mailbox, ImapMailboxSelectOptions { parameters })
588    }
589
590    /// `CLOSE`; expunges deleted messages and unselects the mailbox.
591    pub fn close(&mut self) -> Result<(), ImapClientStdError> {
592        self.run(ImapMailboxClose::new())
593    }
594
595    /// `UNSELECT`; unselects the mailbox without expunging.
596    pub fn unselect(&mut self) -> Result<(), ImapClientStdError> {
597        self.run(ImapMailboxUnselect::new())
598    }
599
600    /// `CHECK`; requests a mailbox checkpoint.
601    pub fn check(&mut self) -> Result<(), ImapClientStdError> {
602        self.run(ImapMailboxCheck::new())
603    }
604
605    /// `EXPUNGE`; returns the expunged sequence numbers.
606    pub fn expunge(&mut self) -> Result<Vec<NonZeroU32>, ImapClientStdError> {
607        self.run(ImapMailboxExpunge::new())
608    }
609
610    /// Consumes the client into a background watcher.
611    ///
612    /// Drop the returned stream (or call its `close`) to wind down.
613    /// Errors when `capability` lacks QRESYNC.
614    pub fn watch_mailbox(
615        self,
616        mailbox: Mailbox<'static>,
617        capability: &[Capability<'static>],
618    ) -> Result<ImapMailboxWatchStream, ImapClientStdError> {
619        let shutdown = Arc::new(AtomicBool::new(false));
620        let mut watcher = ImapMailboxWatch::new(capability, mailbox, shutdown.clone())?;
621        let mut fragmentizer = self.fragmentizer;
622        let mut stream = self.stream;
623
624        let (tx, rx) = mpsc::sync_channel::<Result<ImapMailboxWatchEvent, ImapClientStdError>>(256);
625        let shutdown_handle = shutdown.clone();
626        let handle = thread::spawn(move || {
627            let mut buf = [0u8; READ_BUFFER_SIZE];
628            let mut arg: Option<Vec<u8>> = None;
629
630            loop {
631                match watcher.resume(&mut fragmentizer, arg.as_deref()) {
632                    ImapCoroutineState::Yielded(ImapMailboxWatchYield::Event(e)) => {
633                        arg = None;
634                        if tx.send(Ok(e)).is_err() {
635                            return;
636                        }
637                    }
638                    ImapCoroutineState::Complete(Ok(())) => return,
639                    ImapCoroutineState::Yielded(ImapMailboxWatchYield::WantsRead) => {
640                        match stream.read(&mut buf) {
641                            Ok(0) => {
642                                let eof = io::ErrorKind::UnexpectedEof;
643                                let err = "IMAP server closed the connection during watch";
644                                tx.send(Err(io::Error::new(eof, err).into())).ok();
645                                return;
646                            }
647                            Ok(n) => arg = Some(buf[..n].to_vec()),
648                            Err(err) => {
649                                tx.send(Err(err.into())).ok();
650                                return;
651                            }
652                        }
653                    }
654                    ImapCoroutineState::Yielded(ImapMailboxWatchYield::WantsWrite(bytes)) => {
655                        if let Err(err) = stream.write_all(&bytes) {
656                            tx.send(Err(err.into())).ok();
657                            return;
658                        }
659                        arg = None;
660                    }
661                    ImapCoroutineState::Complete(Err(err)) => {
662                        tx.send(Err(err.into())).ok();
663                        return;
664                    }
665                }
666            }
667        });
668
669        Ok(ImapMailboxWatchStream {
670            rx,
671            handle: Some(handle),
672            shutdown: shutdown_handle,
673        })
674    }
675
676    /// `FETCH`; returns the requested items keyed by message id.
677    pub fn fetch(
678        &mut self,
679        sequence_set: SequenceSet,
680        items: MacroOrMessageDataItemNames<'static>,
681        opts: ImapMessageFetchOptions,
682    ) -> Result<BTreeMap<NonZeroU32, Vec1<MessageDataItem<'static>>>, ImapClientStdError> {
683        self.run(ImapMessageFetch::new(sequence_set, items, opts))
684    }
685
686    /// `FETCH <id> (BODY.PEEK[])` streaming the message body straight
687    /// into `sink`; the body never lands in memory whole.
688    ///
689    /// Peek leaves `\Seen` untouched. Returns once the tagged response
690    /// is parsed; a missing id completes with an empty sink.
691    pub fn fetch_body_stream(
692        &mut self,
693        id: NonZeroU32,
694        uid: bool,
695        mut sink: impl Write,
696    ) -> Result<(), ImapClientStdError> {
697        let mut coroutine = ImapMessageFetchStream::new(id, uid);
698        let mut buf = [0u8; READ_BUFFER_SIZE];
699        let mut arg: Option<&[u8]> = None;
700
701        loop {
702            match coroutine.resume(&mut self.fragmentizer, arg.take()) {
703                ImapCoroutineState::Complete(Ok(())) => return Ok(()),
704                ImapCoroutineState::Complete(Err(err)) => return Err(err.into()),
705                ImapCoroutineState::Yielded(ImapMessageFetchStreamYield::WantsRead) => {
706                    let n = self.stream.read(&mut buf)?;
707                    arg = Some(&buf[..n]);
708                }
709                ImapCoroutineState::Yielded(ImapMessageFetchStreamYield::WantsWrite(bytes)) => {
710                    self.stream.write_all(&bytes)?;
711                    arg = None;
712                }
713                ImapCoroutineState::Yielded(ImapMessageFetchStreamYield::BodyChunk(bytes)) => {
714                    sink.write_all(&bytes)?;
715                    arg = None;
716                }
717                ImapCoroutineState::Yielded(ImapMessageFetchStreamYield::WantsStream { len }) => {
718                    let len = len as u64;
719                    let mut stream = (&mut self.stream).take(len);
720                    let n = io::copy(&mut stream, &mut sink)?;
721                    // NOTE: an empty slice tells the coroutine the
722                    // socket ran short of the declared body length.
723                    arg = (n != len).then_some(&[]);
724                }
725            }
726        }
727    }
728
729    /// `SEARCH`; returns the ids matching `criteria`.
730    pub fn search(
731        &mut self,
732        criteria: Vec1<SearchKey<'static>>,
733        opts: ImapMessageSearchOptions,
734    ) -> Result<Vec<NonZeroU32>, ImapClientStdError> {
735        self.run(ImapMessageSearch::new(criteria, opts))
736    }
737
738    /// `STORE` (echo variant); returns the server-reported FETCH echoes.
739    pub fn store(
740        &mut self,
741        sequence_set: SequenceSet,
742        kind: StoreType,
743        flags: Vec<Flag<'static>>,
744        opts: ImapMessageStoreOptions,
745    ) -> Result<BTreeMap<NonZeroU32, Vec1<MessageDataItem<'static>>>, ImapClientStdError> {
746        self.run(ImapMessageStore::new(sequence_set, kind, flags, opts))
747    }
748
749    /// `COPY`; copies messages to `mailbox` and returns the optional COPYUID
750    /// pair.
751    pub fn copy(
752        &mut self,
753        sequence_set: SequenceSet,
754        mailbox: Mailbox<'static>,
755        opts: ImapMessageCopyOptions,
756    ) -> Result<ImapCopyUid, ImapClientStdError> {
757        self.run(ImapMessageCopy::new(sequence_set, mailbox, opts))
758    }
759
760    /// `MOVE`; moves messages to `mailbox` and returns the optional COPYUID
761    /// pair.
762    pub fn r#move(
763        &mut self,
764        sequence_set: SequenceSet,
765        mailbox: Mailbox<'static>,
766        opts: ImapMessageMoveOptions,
767    ) -> Result<ImapCopyUid, ImapClientStdError> {
768        self.run(ImapMessageMove::new(sequence_set, mailbox, opts))
769    }
770
771    /// `APPEND`; returns the optional EXISTS count and APPENDUID pair.
772    ///
773    /// Buffered: the whole `message` is held in memory. For large
774    /// messages prefer [`Self::append_stream`].
775    pub fn append(
776        &mut self,
777        mailbox: Mailbox<'static>,
778        message: &[u8],
779        opts: ImapMessageAppendOptions,
780    ) -> Result<ImapMessageAppendOutput, ImapClientStdError> {
781        self.run(ImapMessageAppend::new(mailbox, message.to_vec(), opts))
782    }
783
784    /// `APPEND` streaming `len` octets from `source` straight to the
785    /// socket; the body never lands in memory whole.
786    ///
787    /// `len` must match the source exactly: IMAP declares the octet
788    /// count up front, so a shorter source poisons the connection.
789    /// Synchronising by default so the server can reject before the
790    /// body is sent; set `opts.non_sync` to skip the wait.
791    pub fn append_stream(
792        &mut self,
793        mailbox: Mailbox<'static>,
794        mut source: impl Read,
795        len: usize,
796        opts: ImapMessageAppendOptions,
797    ) -> Result<ImapMessageAppendOutput, ImapClientStdError> {
798        let mut coroutine = ImapMessageAppendStream::new(mailbox, len as u32, opts);
799        let mut buf = [0u8; READ_BUFFER_SIZE];
800        let mut arg: Option<&[u8]> = None;
801
802        loop {
803            match coroutine.resume(&mut self.fragmentizer, arg.take()) {
804                ImapCoroutineState::Complete(Ok(out)) => return Ok(out),
805                ImapCoroutineState::Complete(Err(err)) => return Err(err.into()),
806                ImapCoroutineState::Yielded(ImapMessageAppendStreamYield::WantsRead) => {
807                    let n = self.stream.read(&mut buf)?;
808                    arg = Some(&buf[..n]);
809                }
810                ImapCoroutineState::Yielded(ImapMessageAppendStreamYield::WantsWrite(bytes)) => {
811                    self.stream.write_all(&bytes)?;
812                    arg = None;
813                }
814                ImapCoroutineState::Yielded(ImapMessageAppendStreamYield::WantsStream) => {
815                    let len = len as u64;
816                    let mut sink = source.by_ref().take(len);
817                    let n = io::copy(&mut sink, &mut self.stream)?;
818                    // NOTE: an empty slice tells the coroutine the
819                    // source ran short of the declared count.
820                    arg = (n != len).then_some(&[]);
821                }
822            }
823        }
824    }
825
826    /// `SORT` with a client-side fallback.
827    ///
828    /// With `opts.fallback == false` this is a plain server SORT; with
829    /// `opts.fallback == true` it SEARCHes, FETCHes the sort keys, and
830    /// sorts locally. Feed `fallback` from a SORT capability check
831    /// (the server SORT requires the extension).
832    pub fn sort(
833        &mut self,
834        sort_criteria: Vec1<SortCriterion>,
835        search_criteria: Vec1<SearchKey<'static>>,
836        opts: ImapMessageSortOptions,
837    ) -> Result<Vec<NonZeroU32>, ImapClientStdError> {
838        self.run(ImapMessageSort::new(sort_criteria, search_criteria, opts))
839    }
840
841    /// `THREAD`; returns the message threads matching `search_criteria`.
842    pub fn thread(
843        &mut self,
844        algorithm: ThreadingAlgorithm<'static>,
845        search_criteria: Vec1<SearchKey<'static>>,
846        opts: ImapMessageThreadOptions,
847    ) -> Result<Vec<Thread>, ImapClientStdError> {
848        self.run(ImapMessageThread::new(algorithm, search_criteria, opts))
849    }
850}
851
852impl fmt::Debug for ImapClientStd {
853    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
854        f.debug_struct("ImapClientStd")
855            .field("fragmentizer", &self.fragmentizer)
856            .finish_non_exhaustive()
857    }
858}
859
860/// Background-worker watch stream; drop or [`Self::close`] to wind down.
861pub struct ImapMailboxWatchStream {
862    rx: Receiver<Result<ImapMailboxWatchEvent, ImapClientStdError>>,
863    handle: Option<JoinHandle<()>>,
864    shutdown: Arc<AtomicBool>,
865}
866
867impl ImapMailboxWatchStream {
868    /// Non-blocking probe for the next event.
869    pub fn try_recv(
870        &self,
871    ) -> Result<Result<ImapMailboxWatchEvent, ImapClientStdError>, TryRecvError> {
872        self.rx.try_recv()
873    }
874
875    /// Waits up to `timeout` for the next event.
876    pub fn recv_timeout(
877        &self,
878        timeout: Duration,
879    ) -> Result<Result<ImapMailboxWatchEvent, ImapClientStdError>, RecvTimeoutError> {
880        self.rx.recv_timeout(timeout)
881    }
882
883    /// Signals shutdown and joins the worker.
884    pub fn close(mut self) -> Result<(), ImapClientStdError> {
885        self.shutdown.store(true, Ordering::SeqCst);
886        if let Some(handle) = self.handle.take() {
887            handle
888                .join()
889                .map_err(|_| io::Error::other("IMAP watch worker panicked"))?;
890        }
891        Ok(())
892    }
893}
894
895impl Iterator for ImapMailboxWatchStream {
896    type Item = Result<ImapMailboxWatchEvent, ImapClientStdError>;
897
898    fn next(&mut self) -> Option<Self::Item> {
899        self.rx.recv().ok()
900    }
901}
902
903impl Drop for ImapMailboxWatchStream {
904    fn drop(&mut self) {
905        self.shutdown.store(true, Ordering::SeqCst);
906
907        if let Some(handle) = self.handle.take() {
908            handle.join().ok();
909        }
910    }
911}
912
913#[cfg(any(
914    feature = "rustls-aws",
915    feature = "rustls-ring",
916    feature = "native-tls"
917))]
918impl ImapClientStd {
919    /// End-to-end connect: TCP/TLS, optional STARTTLS, greeting,
920    /// optional SASL.
921    ///
922    /// `imap://` is plain TCP (143), `imaps://` is implicit TLS (993).
923    /// `starttls = true` is only valid on `imap://`. Pass `Sasl::None`
924    /// to skip auth.
925    pub fn connect(
926        url: &Url,
927        tls: &Tls,
928        starttls: bool,
929        sasl: Option<impl Into<Sasl>>,
930        auto_id: Option<Vec<(IString<'static>, NString<'static>)>>,
931    ) -> Result<(Self, Vec<Capability<'static>>), ImapClientStdError> {
932        let Some(host) = url.host_str() else {
933            return Err(ImapClientStdError::UrlMissingHost(url.to_string()));
934        };
935
936        let (stream, is_tls) = match url.scheme() {
937            scheme if scheme.eq_ignore_ascii_case("imap") => (
938                StreamStd::connect_tcp(host, url.port().unwrap_or(143))?,
939                false,
940            ),
941            scheme if scheme.eq_ignore_ascii_case("imaps") => (
942                StreamStd::connect_tls(host, url.port().unwrap_or(993), tls)?,
943                true,
944            ),
945            scheme => {
946                let url = url.to_string();
947                let scheme = scheme.to_string();
948                return Err(ImapClientStdError::UrlUnsupportedScheme(url, scheme));
949            }
950        };
951
952        if starttls && is_tls {
953            return Err(ImapClientStdError::StartTlsOverTls);
954        }
955
956        // NOTE: STARTTLS needs the concrete StreamStd for upgrade_tls,
957        // so run it inline before boxing the stream.
958        let stream = if starttls {
959            let mut stream = stream;
960            let mut fragmentizer = Fragmentizer::new(FRAGMENTIZER_MAX_MESSAGE_SIZE);
961            run_starttls(&mut stream, &mut fragmentizer)?;
962            stream.upgrade_tls(tls)?
963        } else {
964            stream
965        };
966
967        // NOTE: 5s per-read timeout lets watch_mailbox poll shutdown
968        // during a silent IDLE; long FETCHes are unaffected.
969        stream.set_read_timeout(Some(Duration::from_secs(5)))?;
970
971        let mut client = Self::new(stream);
972        client.auto_id = auto_id;
973
974        let mut capability = if starttls {
975            client.capability()?
976        } else {
977            client.greeting()?
978        };
979
980        if let Some(sasl) = sasl.map(Into::into) {
981            let ir = capability.contains(&Capability::SaslIr);
982
983            capability = match sasl {
984                Sasl::Anonymous(SaslAnonymous { message }) => {
985                    let opts = ImapAuthAnonymousOptions {
986                        initial_request: ir,
987                        ensure_capabilities: true,
988                        auto_id: client.auto_id.take(),
989                    };
990
991                    client.auth_anonymous(message, opts)?
992                }
993                Sasl::Login(SaslLogin { username, password }) => {
994                    let opts = ImapLoginOptions {
995                        ensure_capabilities: true,
996                        auto_id: client.auto_id.take(),
997                    };
998
999                    client.login(username, password.expose_secret(), opts)?
1000                }
1001                Sasl::Plain(SaslPlain {
1002                    authzid,
1003                    authcid,
1004                    passwd,
1005                }) => {
1006                    let opts = ImapAuthPlainOptions {
1007                        initial_request: ir,
1008                        ensure_capabilities: true,
1009                        auto_id: client.auto_id.take(),
1010                    };
1011
1012                    client.auth_plain(authzid, authcid, passwd.expose_secret(), opts)?
1013                }
1014                Sasl::Oauthbearer(SaslOauthbearer {
1015                    username,
1016                    host,
1017                    port,
1018                    token,
1019                }) => {
1020                    let opts = ImapAuthOauthbearerOptions {
1021                        initial_request: ir,
1022                        ensure_capabilities: true,
1023                        auto_id: client.auto_id.take(),
1024                    };
1025
1026                    client.auth_oauthbearer(username, host, port, token.expose_secret(), opts)?
1027                }
1028                Sasl::Xoauth2(SaslXoauth2 { username, token }) => {
1029                    let opts = ImapAuthXoauth2Options {
1030                        initial_request: ir,
1031                        ensure_capabilities: true,
1032                        auto_id: client.auto_id.take(),
1033                    };
1034
1035                    client.auth_xoauth2(username, token.expose_secret(), opts)?
1036                }
1037                #[cfg(feature = "scram")]
1038                Sasl::ScramSha256(SaslScramSha256 { username, password }) => {
1039                    let opts = ImapAuthScramSha256Options {
1040                        initial_request: ir,
1041                        ensure_capabilities: true,
1042                        auto_id: client.auto_id.take(),
1043                    };
1044
1045                    client.auth_scram_sha256(username, password.expose_secret(), opts)?
1046                }
1047                #[cfg(not(feature = "scram"))]
1048                Sasl::ScramSha256(_) => {
1049                    return Err(ImapClientStdError::ScramSha256NotEnabled);
1050                }
1051            };
1052        }
1053
1054        Ok((client, capability))
1055    }
1056}
1057
1058/// Inline STARTTLS loop: keeps the concrete `StreamStd` so that
1059/// `upgrade_tls` can swap the underlying socket afterwards.
1060#[cfg(any(
1061    feature = "rustls-aws",
1062    feature = "rustls-ring",
1063    feature = "native-tls"
1064))]
1065fn run_starttls(
1066    stream: &mut StreamStd,
1067    fragmentizer: &mut Fragmentizer,
1068) -> Result<(), ImapClientStdError> {
1069    let mut coroutine = ImapStartTls::new();
1070    let mut buf = [0u8; READ_BUFFER_SIZE];
1071    let mut arg: Option<&[u8]> = None;
1072
1073    loop {
1074        match coroutine.resume(fragmentizer, arg.take()) {
1075            ImapCoroutineState::Complete(Ok(_)) => return Ok(()),
1076            ImapCoroutineState::Complete(Err(err)) => return Err(err.into()),
1077            ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
1078                let n = stream.read(&mut buf)?;
1079                arg = Some(&buf[..n]);
1080            }
1081            ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
1082                stream.write_all(&bytes)?;
1083            }
1084        }
1085    }
1086}
1087
1088/// Blocking stream the client runs over, auto-implemented for any
1089/// `Read + Write + Send + 'static`.
1090///
1091/// `as_any_mut` supports downcasting back to the concrete stream when
1092/// needed (e.g. for `set_read_timeout`).
1093pub trait ImapStream: Read + Write + Send + Any {
1094    /// The stream as a mutable `Any`, ready for downcasting.
1095    fn as_any_mut(&mut self) -> &mut dyn Any;
1096}
1097
1098impl<T: Read + Write + Send + Any> ImapStream for T {
1099    fn as_any_mut(&mut self) -> &mut dyn Any {
1100        self
1101    }
1102}