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/// Default IMAP port for `scheme`: 993 for `imaps`, 143 otherwise.
290pub fn default_port(scheme: &str) -> u16 {
291    if scheme.eq_ignore_ascii_case("imaps") {
292        993
293    } else {
294        143
295    }
296}
297
298/// Blocking IMAP client: a stream, the connection-wide `Fragmentizer`
299/// and one method per coroutine.
300pub struct ImapClientStd {
301    /// The stream carrying the connection to the IMAP server.
302    pub stream: Box<dyn ImapStream>,
303    /// The connection-wide parser buffer shared by every coroutine run
304    /// on this connection.
305    pub fragmentizer: Fragmentizer,
306    /// ID parameters consumed by every auth_*/login call; required by
307    /// a few providers (mail.qq.com, fastmail).
308    ///
309    /// `None` skips, `Some(empty)` sends `ID NIL`, `Some(params)`
310    /// sends `ID (k v ...)`.
311    pub auto_id: Option<Vec<(IString<'static>, NString<'static>)>>,
312    /// Whether the server greeting was `PREAUTH`: the session opened
313    /// already authenticated (a socket proxy such as sirup), so
314    /// [`connect`](Self::connect) skipped the SASL step. Stays `false`
315    /// on a freshly-opened connection.
316    pub pre_authenticated: bool,
317}
318
319impl ImapClientStd {
320    /// Caller is responsible for opening the connection (TCP, TLS,
321    /// STARTTLS).
322    pub fn new<S: Read + Write + Send + 'static>(stream: S) -> Self {
323        Self {
324            stream: Box::new(stream),
325            fragmentizer: Fragmentizer::new(FRAGMENTIZER_MAX_MESSAGE_SIZE),
326            auto_id: None,
327            pre_authenticated: false,
328        }
329    }
330
331    /// Useful after a STARTTLS upgrade or on reconnection.
332    pub fn set_stream<S: Read + Write + Send + 'static>(&mut self, stream: S) {
333        self.stream = Box::new(stream);
334    }
335
336    /// Runs a standard-shape coroutine to completion, fulfilling its
337    /// read and write requests.
338    ///
339    /// Richer yields (IDLE events, watch deltas, streamed bodies) need
340    /// their own per-method loops.
341    pub fn run<C, T, E>(&mut self, mut coroutine: C) -> Result<T, ImapClientStdError>
342    where
343        C: ImapCoroutine<Yield = ImapYield, Return = Result<T, E>>,
344        ImapClientStdError: From<E>,
345    {
346        let mut buf = [0u8; READ_BUFFER_SIZE];
347        let mut arg: Option<&[u8]> = None;
348
349        loop {
350            match coroutine.resume(&mut self.fragmentizer, arg.take()) {
351                ImapCoroutineState::Complete(Ok(out)) => return Ok(out),
352                ImapCoroutineState::Complete(Err(err)) => return Err(err.into()),
353                ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
354                    let n = self.stream.read(&mut buf)?;
355                    // NOTE: a zero-length read is EOF; error out instead of
356                    // feeding the coroutine an empty buffer forever.
357                    if n == 0 {
358                        let kind = io::ErrorKind::UnexpectedEof;
359                        let err = io::Error::new(kind, "IMAP server closed the connection");
360                        return Err(err.into());
361                    }
362                    arg = Some(&buf[..n]);
363                }
364                ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
365                    self.stream.write_all(&bytes)?;
366                    arg = None;
367                }
368            }
369        }
370    }
371
372    /// Consumes the greeting and returns the advertised capabilities
373    /// (forcing a CAPABILITY round-trip if the greeting carried none).
374    pub fn greeting(&mut self) -> Result<Vec<Capability<'static>>, ImapClientStdError> {
375        Ok(self
376            .run(ImapGreetingGet::new(ImapGreetingGetOptions {
377                ensure_capabilities: true,
378            }))?
379            .capability)
380    }
381
382    /// `LOGIN`. Channel must be TLS-protected. Consumes `auto_id`.
383    pub fn login(
384        &mut self,
385        user: impl AsRef<str>,
386        password: impl AsRef<str>,
387        opts: ImapLoginOptions,
388    ) -> Result<Vec<Capability<'static>>, ImapClientStdError> {
389        self.run(ImapLogin::new(user, password, opts)?)
390    }
391
392    /// `STARTTLS`. Caller still has to upgrade the socket and refresh
393    /// capabilities.
394    ///
395    /// Returns any bytes pre-read past the tagged response; a
396    /// non-empty return is a STARTTLS-injection signal, refuse the
397    /// upgrade.
398    pub fn starttls(&mut self) -> Result<Vec<u8>, ImapClientStdError> {
399        self.run(ImapStartTls::new())
400    }
401
402    /// SASL `AUTHENTICATE ANONYMOUS`. Consumes `auto_id`.
403    pub fn auth_anonymous(
404        &mut self,
405        message: Option<impl AsRef<str>>,
406        opts: ImapAuthAnonymousOptions,
407    ) -> Result<Vec<Capability<'static>>, ImapClientStdError> {
408        self.run(ImapAuthAnonymous::new(message, opts))
409    }
410
411    /// SASL `AUTHENTICATE LOGIN` (legacy). Prefer auth_plain or
412    /// auth_scram_sha256 when supported. Consumes `auto_id`.
413    pub fn auth_login(
414        &mut self,
415        user: impl AsRef<str>,
416        password: impl AsRef<str>,
417        opts: ImapAuthLoginOptions,
418    ) -> Result<Vec<Capability<'static>>, ImapClientStdError> {
419        self.run(ImapAuthLogin::new(user, password, opts))
420    }
421
422    /// SASL `AUTHENTICATE PLAIN`. Consumes `auto_id`.
423    pub fn auth_plain(
424        &mut self,
425        authzid: Option<impl AsRef<str>>,
426        authcid: impl AsRef<str>,
427        password: impl AsRef<str>,
428        opts: ImapAuthPlainOptions,
429    ) -> Result<Vec<Capability<'static>>, ImapClientStdError> {
430        self.run(ImapAuthPlain::new(authzid, authcid, password, opts))
431    }
432
433    /// SASL `AUTHENTICATE OAUTHBEARER`. Channel must be
434    /// TLS-protected. Consumes `auto_id`.
435    pub fn auth_oauthbearer(
436        &mut self,
437        user: impl AsRef<str>,
438        host: impl AsRef<str>,
439        port: u16,
440        token: impl AsRef<str>,
441        opts: ImapAuthOauthbearerOptions,
442    ) -> Result<Vec<Capability<'static>>, ImapClientStdError> {
443        self.run(ImapAuthOauthbearer::new(user, host, port, token, opts))
444    }
445
446    /// SASL `AUTHENTICATE XOAUTH2` (Google's pre-standard mechanism).
447    /// Prefer auth_oauthbearer when supported. Consumes `auto_id`.
448    pub fn auth_xoauth2(
449        &mut self,
450        user: impl AsRef<str>,
451        token: impl AsRef<str>,
452        opts: ImapAuthXoauth2Options,
453    ) -> Result<Vec<Capability<'static>>, ImapClientStdError> {
454        self.run(ImapAuthXoauth2::new(user, token, opts))
455    }
456
457    /// SASL `AUTHENTICATE SCRAM-SHA-256`. Consumes `auto_id`.
458    #[cfg(feature = "scram")]
459    pub fn auth_scram_sha256(
460        &mut self,
461        user: impl AsRef<str>,
462        password: impl AsRef<str>,
463        opts: ImapAuthScramSha256Options,
464    ) -> Result<Vec<Capability<'static>>, ImapClientStdError> {
465        self.run(ImapAuthScramSha256::new(user, password, opts))
466    }
467
468    /// `LOGOUT`; ends the session.
469    pub fn logout(&mut self) -> Result<(), ImapClientStdError> {
470        self.run(ImapLogout::new())
471    }
472
473    /// `CAPABILITY`; returns the advertised capabilities.
474    pub fn capability(&mut self) -> Result<Vec<Capability<'static>>, ImapClientStdError> {
475        self.run(ImapCapabilityGet::new())
476    }
477
478    /// `NOOP`; round-trips to keep the connection alive or poll for updates.
479    pub fn noop(&mut self) -> Result<(), ImapClientStdError> {
480        self.run(ImapNoop::new())
481    }
482
483    /// Sends an arbitrary raw command line (no tag, no trailing CRLF)
484    /// and returns the verbatim server response.
485    ///
486    /// The response spans up to and including the tagged completion
487    /// line. Synchronizing literals are not supported.
488    pub fn raw(&mut self, command: impl AsRef<str>) -> Result<String, ImapClientStdError> {
489        self.run(ImapRaw::new(command))
490    }
491
492    /// `ID`. An `opts.parameters` of `None` sends `ID NIL`.
493    pub fn id(
494        &mut self,
495        opts: ImapServerIdOptions,
496    ) -> Result<Option<Vec<(IString<'static>, NString<'static>)>>, ImapClientStdError> {
497        self.run(ImapServerId::new(opts))
498    }
499
500    /// `ENABLE`; returns the capabilities the server confirmed enabling.
501    pub fn enable(
502        &mut self,
503        capabilities: Vec1<CapabilityEnable<'static>>,
504    ) -> Result<Option<Vec<CapabilityEnable<'static>>>, ImapClientStdError> {
505        self.run(ImapExtensionEnable::new(capabilities))
506    }
507
508    /// `LIST`; returns the mailboxes matching `reference` and `pattern`.
509    pub fn list(
510        &mut self,
511        reference: Mailbox<'static>,
512        pattern: ListMailbox<'static>,
513    ) -> Result<ImapMailboxListing, ImapClientStdError> {
514        self.run(ImapMailboxList::new(reference, pattern))
515    }
516
517    /// `LSUB`; returns the subscribed mailboxes matching `reference` and
518    /// `pattern`.
519    pub fn lsub(
520        &mut self,
521        reference: Mailbox<'static>,
522        pattern: ListMailbox<'static>,
523    ) -> Result<ImapMailboxListing, ImapClientStdError> {
524        self.run(ImapMailboxLsub::new(reference, pattern))
525    }
526
527    /// `STATUS`; returns the requested status items for `mailbox`.
528    pub fn status(
529        &mut self,
530        mailbox: Mailbox<'static>,
531        item_names: impl Into<Cow<'static, [StatusDataItemName]>>,
532    ) -> Result<Vec<StatusDataItem>, ImapClientStdError> {
533        self.run(ImapMailboxStatus::new(mailbox, item_names))
534    }
535
536    /// `CREATE`; creates `mailbox`.
537    pub fn create(&mut self, mailbox: Mailbox<'static>) -> Result<(), ImapClientStdError> {
538        self.run(ImapMailboxCreate::new(mailbox))
539    }
540
541    /// `DELETE`; deletes `mailbox`.
542    pub fn delete(&mut self, mailbox: Mailbox<'static>) -> Result<(), ImapClientStdError> {
543        self.run(ImapMailboxDelete::new(mailbox))
544    }
545
546    /// `RENAME`; renames mailbox `from` to `to`.
547    pub fn rename(
548        &mut self,
549        from: Mailbox<'static>,
550        to: Mailbox<'static>,
551    ) -> Result<(), ImapClientStdError> {
552        self.run(ImapMailboxRename::new(from, to))
553    }
554
555    /// `SUBSCRIBE`; subscribes to `mailbox`.
556    pub fn subscribe(&mut self, mailbox: Mailbox<'static>) -> Result<(), ImapClientStdError> {
557        self.run(ImapMailboxSubscribe::new(mailbox))
558    }
559
560    /// `UNSUBSCRIBE`; unsubscribes from `mailbox`.
561    pub fn unsubscribe(&mut self, mailbox: Mailbox<'static>) -> Result<(), ImapClientStdError> {
562        self.run(ImapMailboxUnsubscribe::new(mailbox))
563    }
564
565    /// `SELECT`; opens `mailbox` for read-write and returns its state.
566    pub fn select(
567        &mut self,
568        mailbox: Mailbox<'static>,
569        opts: ImapMailboxSelectOptions,
570    ) -> Result<ImapMailboxSelectData, ImapClientStdError> {
571        self.run(ImapMailboxSelect::new(mailbox, opts))
572    }
573
574    /// `EXAMINE`; opens `mailbox` read-only and returns its state.
575    pub fn examine(
576        &mut self,
577        mailbox: Mailbox<'static>,
578        opts: ImapMailboxExamineOptions,
579    ) -> Result<ImapMailboxSelectData, ImapClientStdError> {
580        self.run(ImapMailboxExamine::new(mailbox, opts))
581    }
582
583    /// `SELECT <mailbox> (QRESYNC ...)`.
584    ///
585    /// Errors with `QresyncNotSupported` when `capability` lacks
586    /// QRESYNC, with `InvalidModSeq` when `highest_mod_seq` is 0.
587    pub fn select_qresync(
588        &mut self,
589        mailbox: Mailbox<'static>,
590        uid_validity: NonZeroU32,
591        highest_mod_seq: u64,
592        capability: &[Capability<'static>],
593    ) -> Result<ImapMailboxSelectData, ImapClientStdError> {
594        if !capability.contains(&Capability::QResync) {
595            return Err(ImapClientStdError::QresyncNotSupported);
596        }
597
598        let Some(highest_mod_seq) = NonZeroU64::new(highest_mod_seq) else {
599            return Err(ImapClientStdError::InvalidModSeq);
600        };
601
602        let parameters = vec![SelectParameter::QResync {
603            uid_validity,
604            mod_sequence_value: highest_mod_seq,
605            known_uids: None,
606            seq_match_data: None,
607        }];
608
609        self.select(mailbox, ImapMailboxSelectOptions { parameters })
610    }
611
612    /// `CLOSE`; expunges deleted messages and unselects the mailbox.
613    pub fn close(&mut self) -> Result<(), ImapClientStdError> {
614        self.run(ImapMailboxClose::new())
615    }
616
617    /// `UNSELECT`; unselects the mailbox without expunging.
618    pub fn unselect(&mut self) -> Result<(), ImapClientStdError> {
619        self.run(ImapMailboxUnselect::new())
620    }
621
622    /// `CHECK`; requests a mailbox checkpoint.
623    pub fn check(&mut self) -> Result<(), ImapClientStdError> {
624        self.run(ImapMailboxCheck::new())
625    }
626
627    /// `EXPUNGE`; returns the expunged sequence numbers.
628    pub fn expunge(&mut self) -> Result<Vec<NonZeroU32>, ImapClientStdError> {
629        self.run(ImapMailboxExpunge::new())
630    }
631
632    /// Consumes the client into a background watcher.
633    ///
634    /// Drop the returned stream (or call its `close`) to wind down.
635    /// Errors when `capability` lacks QRESYNC.
636    pub fn watch_mailbox(
637        self,
638        mailbox: Mailbox<'static>,
639        capability: &[Capability<'static>],
640    ) -> Result<ImapMailboxWatchStream, ImapClientStdError> {
641        let shutdown = Arc::new(AtomicBool::new(false));
642        let mut watcher = ImapMailboxWatch::new(capability, mailbox, shutdown.clone())?;
643        let mut fragmentizer = self.fragmentizer;
644        let mut stream = self.stream;
645
646        let (tx, rx) = mpsc::sync_channel::<Result<ImapMailboxWatchEvent, ImapClientStdError>>(256);
647        let shutdown_handle = shutdown.clone();
648        let handle = thread::spawn(move || {
649            let mut buf = [0u8; READ_BUFFER_SIZE];
650            let mut arg: Option<Vec<u8>> = None;
651
652            loop {
653                match watcher.resume(&mut fragmentizer, arg.as_deref()) {
654                    ImapCoroutineState::Yielded(ImapMailboxWatchYield::Event(e)) => {
655                        arg = None;
656                        if tx.send(Ok(e)).is_err() {
657                            return;
658                        }
659                    }
660                    ImapCoroutineState::Complete(Ok(())) => return,
661                    ImapCoroutineState::Yielded(ImapMailboxWatchYield::WantsRead) => {
662                        match stream.read(&mut buf) {
663                            Ok(0) => {
664                                let eof = io::ErrorKind::UnexpectedEof;
665                                let err = "IMAP server closed the connection during watch";
666                                tx.send(Err(io::Error::new(eof, err).into())).ok();
667                                return;
668                            }
669                            Ok(n) => arg = Some(buf[..n].to_vec()),
670                            Err(err) => {
671                                tx.send(Err(err.into())).ok();
672                                return;
673                            }
674                        }
675                    }
676                    ImapCoroutineState::Yielded(ImapMailboxWatchYield::WantsWrite(bytes)) => {
677                        if let Err(err) = stream.write_all(&bytes) {
678                            tx.send(Err(err.into())).ok();
679                            return;
680                        }
681                        arg = None;
682                    }
683                    ImapCoroutineState::Complete(Err(err)) => {
684                        tx.send(Err(err.into())).ok();
685                        return;
686                    }
687                }
688            }
689        });
690
691        Ok(ImapMailboxWatchStream {
692            rx,
693            handle: Some(handle),
694            shutdown: shutdown_handle,
695        })
696    }
697
698    /// `FETCH`; returns the requested items keyed by message id.
699    pub fn fetch(
700        &mut self,
701        sequence_set: SequenceSet,
702        items: MacroOrMessageDataItemNames<'static>,
703        opts: ImapMessageFetchOptions,
704    ) -> Result<BTreeMap<NonZeroU32, Vec1<MessageDataItem<'static>>>, ImapClientStdError> {
705        self.run(ImapMessageFetch::new(sequence_set, items, opts))
706    }
707
708    /// `FETCH <id> (BODY.PEEK[])` streaming the message body straight
709    /// into `sink`; the body never lands in memory whole.
710    ///
711    /// Peek leaves `\Seen` untouched. Returns once the tagged response
712    /// is parsed; a missing id completes with an empty sink.
713    pub fn fetch_body_stream(
714        &mut self,
715        id: NonZeroU32,
716        uid: bool,
717        mut sink: impl Write,
718    ) -> Result<(), ImapClientStdError> {
719        let mut coroutine = ImapMessageFetchStream::new(id, uid);
720        let mut buf = [0u8; READ_BUFFER_SIZE];
721        let mut arg: Option<&[u8]> = None;
722
723        loop {
724            match coroutine.resume(&mut self.fragmentizer, arg.take()) {
725                ImapCoroutineState::Complete(Ok(())) => return Ok(()),
726                ImapCoroutineState::Complete(Err(err)) => return Err(err.into()),
727                ImapCoroutineState::Yielded(ImapMessageFetchStreamYield::WantsRead) => {
728                    let n = self.stream.read(&mut buf)?;
729                    arg = Some(&buf[..n]);
730                }
731                ImapCoroutineState::Yielded(ImapMessageFetchStreamYield::WantsWrite(bytes)) => {
732                    self.stream.write_all(&bytes)?;
733                    arg = None;
734                }
735                ImapCoroutineState::Yielded(ImapMessageFetchStreamYield::BodyChunk(bytes)) => {
736                    sink.write_all(&bytes)?;
737                    arg = None;
738                }
739                ImapCoroutineState::Yielded(ImapMessageFetchStreamYield::WantsStream { len }) => {
740                    let len = len as u64;
741                    let mut stream = (&mut self.stream).take(len);
742                    let n = io::copy(&mut stream, &mut sink)?;
743                    // NOTE: an empty slice tells the coroutine the
744                    // socket ran short of the declared body length.
745                    arg = (n != len).then_some(&[]);
746                }
747            }
748        }
749    }
750
751    /// `SEARCH`; returns the ids matching `criteria`.
752    pub fn search(
753        &mut self,
754        criteria: Vec1<SearchKey<'static>>,
755        opts: ImapMessageSearchOptions,
756    ) -> Result<Vec<NonZeroU32>, ImapClientStdError> {
757        self.run(ImapMessageSearch::new(criteria, opts))
758    }
759
760    /// `STORE` (echo variant); returns the server-reported FETCH echoes.
761    pub fn store(
762        &mut self,
763        sequence_set: SequenceSet,
764        kind: StoreType,
765        flags: Vec<Flag<'static>>,
766        opts: ImapMessageStoreOptions,
767    ) -> Result<BTreeMap<NonZeroU32, Vec1<MessageDataItem<'static>>>, ImapClientStdError> {
768        self.run(ImapMessageStore::new(sequence_set, kind, flags, opts))
769    }
770
771    /// `COPY`; copies messages to `mailbox` and returns the optional COPYUID
772    /// pair.
773    pub fn copy(
774        &mut self,
775        sequence_set: SequenceSet,
776        mailbox: Mailbox<'static>,
777        opts: ImapMessageCopyOptions,
778    ) -> Result<ImapCopyUid, ImapClientStdError> {
779        self.run(ImapMessageCopy::new(sequence_set, mailbox, opts))
780    }
781
782    /// `MOVE`; moves messages to `mailbox` and returns the optional COPYUID
783    /// pair.
784    pub fn r#move(
785        &mut self,
786        sequence_set: SequenceSet,
787        mailbox: Mailbox<'static>,
788        opts: ImapMessageMoveOptions,
789    ) -> Result<ImapCopyUid, ImapClientStdError> {
790        self.run(ImapMessageMove::new(sequence_set, mailbox, opts))
791    }
792
793    /// `APPEND`; returns the optional EXISTS count and APPENDUID pair.
794    ///
795    /// Buffered: the whole `message` is held in memory. For large
796    /// messages prefer [`Self::append_stream`].
797    pub fn append(
798        &mut self,
799        mailbox: Mailbox<'static>,
800        message: &[u8],
801        opts: ImapMessageAppendOptions,
802    ) -> Result<ImapMessageAppendOutput, ImapClientStdError> {
803        self.run(ImapMessageAppend::new(mailbox, message.to_vec(), opts))
804    }
805
806    /// `APPEND` streaming `len` octets from `source` straight to the
807    /// socket; the body never lands in memory whole.
808    ///
809    /// `len` must match the source exactly: IMAP declares the octet
810    /// count up front, so a shorter source poisons the connection.
811    /// Synchronising by default so the server can reject before the
812    /// body is sent; set `opts.non_sync` to skip the wait.
813    pub fn append_stream(
814        &mut self,
815        mailbox: Mailbox<'static>,
816        mut source: impl Read,
817        len: usize,
818        opts: ImapMessageAppendOptions,
819    ) -> Result<ImapMessageAppendOutput, ImapClientStdError> {
820        let mut coroutine = ImapMessageAppendStream::new(mailbox, len as u32, opts);
821        let mut buf = [0u8; READ_BUFFER_SIZE];
822        let mut arg: Option<&[u8]> = None;
823
824        loop {
825            match coroutine.resume(&mut self.fragmentizer, arg.take()) {
826                ImapCoroutineState::Complete(Ok(out)) => return Ok(out),
827                ImapCoroutineState::Complete(Err(err)) => return Err(err.into()),
828                ImapCoroutineState::Yielded(ImapMessageAppendStreamYield::WantsRead) => {
829                    let n = self.stream.read(&mut buf)?;
830                    arg = Some(&buf[..n]);
831                }
832                ImapCoroutineState::Yielded(ImapMessageAppendStreamYield::WantsWrite(bytes)) => {
833                    self.stream.write_all(&bytes)?;
834                    arg = None;
835                }
836                ImapCoroutineState::Yielded(ImapMessageAppendStreamYield::WantsStream) => {
837                    let len = len as u64;
838                    let mut sink = source.by_ref().take(len);
839                    let n = io::copy(&mut sink, &mut self.stream)?;
840                    // NOTE: an empty slice tells the coroutine the
841                    // source ran short of the declared count.
842                    arg = (n != len).then_some(&[]);
843                }
844            }
845        }
846    }
847
848    /// `SORT` with a client-side fallback.
849    ///
850    /// With `opts.fallback == false` this is a plain server SORT; with
851    /// `opts.fallback == true` it SEARCHes, FETCHes the sort keys, and
852    /// sorts locally. Feed `fallback` from a SORT capability check
853    /// (the server SORT requires the extension).
854    pub fn sort(
855        &mut self,
856        sort_criteria: Vec1<SortCriterion>,
857        search_criteria: Vec1<SearchKey<'static>>,
858        opts: ImapMessageSortOptions,
859    ) -> Result<Vec<NonZeroU32>, ImapClientStdError> {
860        self.run(ImapMessageSort::new(sort_criteria, search_criteria, opts))
861    }
862
863    /// `THREAD`; returns the message threads matching `search_criteria`.
864    pub fn thread(
865        &mut self,
866        algorithm: ThreadingAlgorithm<'static>,
867        search_criteria: Vec1<SearchKey<'static>>,
868        opts: ImapMessageThreadOptions,
869    ) -> Result<Vec<Thread>, ImapClientStdError> {
870        self.run(ImapMessageThread::new(algorithm, search_criteria, opts))
871    }
872}
873
874impl fmt::Debug for ImapClientStd {
875    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
876        f.debug_struct("ImapClientStd")
877            .field("fragmentizer", &self.fragmentizer)
878            .finish_non_exhaustive()
879    }
880}
881
882/// Background-worker watch stream; drop or [`Self::close`] to wind down.
883pub struct ImapMailboxWatchStream {
884    rx: Receiver<Result<ImapMailboxWatchEvent, ImapClientStdError>>,
885    handle: Option<JoinHandle<()>>,
886    shutdown: Arc<AtomicBool>,
887}
888
889impl ImapMailboxWatchStream {
890    /// Non-blocking probe for the next event.
891    pub fn try_recv(
892        &self,
893    ) -> Result<Result<ImapMailboxWatchEvent, ImapClientStdError>, TryRecvError> {
894        self.rx.try_recv()
895    }
896
897    /// Waits up to `timeout` for the next event.
898    pub fn recv_timeout(
899        &self,
900        timeout: Duration,
901    ) -> Result<Result<ImapMailboxWatchEvent, ImapClientStdError>, RecvTimeoutError> {
902        self.rx.recv_timeout(timeout)
903    }
904
905    /// Signals shutdown and joins the worker.
906    pub fn close(mut self) -> Result<(), ImapClientStdError> {
907        self.shutdown.store(true, Ordering::SeqCst);
908        if let Some(handle) = self.handle.take() {
909            handle
910                .join()
911                .map_err(|_| io::Error::other("IMAP watch worker panicked"))?;
912        }
913        Ok(())
914    }
915}
916
917impl Iterator for ImapMailboxWatchStream {
918    type Item = Result<ImapMailboxWatchEvent, ImapClientStdError>;
919
920    fn next(&mut self) -> Option<Self::Item> {
921        self.rx.recv().ok()
922    }
923}
924
925impl Drop for ImapMailboxWatchStream {
926    fn drop(&mut self) {
927        self.shutdown.store(true, Ordering::SeqCst);
928
929        if let Some(handle) = self.handle.take() {
930            handle.join().ok();
931        }
932    }
933}
934
935#[cfg(any(
936    feature = "rustls-aws",
937    feature = "rustls-ring",
938    feature = "native-tls"
939))]
940impl ImapClientStd {
941    /// End-to-end connect: TCP/TLS, optional STARTTLS, greeting,
942    /// optional SASL.
943    ///
944    /// `imap://` is plain TCP (143), `imaps://` is implicit TLS (993).
945    /// `starttls = true` is only valid on `imap://`. Pass `Sasl::None`
946    /// to skip auth.
947    pub fn connect(
948        url: &Url,
949        tls: &Tls,
950        starttls: bool,
951        sasl: Option<impl Into<Sasl>>,
952        auto_id: Option<Vec<(IString<'static>, NString<'static>)>>,
953    ) -> Result<(Self, Vec<Capability<'static>>), ImapClientStdError> {
954        let (stream, is_tls) = match url.scheme() {
955            scheme if scheme.eq_ignore_ascii_case("imap") => {
956                let host = tcp_host(url)?;
957                (
958                    StreamStd::connect_tcp(host, url.port().unwrap_or(default_port(scheme)))?,
959                    false,
960                )
961            }
962            scheme if scheme.eq_ignore_ascii_case("imaps") => {
963                let host = tcp_host(url)?;
964                (
965                    StreamStd::connect_tls(host, url.port().unwrap_or(default_port(scheme)), tls)?,
966                    true,
967                )
968            }
969            // NOTE: a `unix://` URL reaches a local socket proxy such as
970            // sirup: no host, no TLS, and the session is usually already
971            // authenticated (see the PREAUTH handling below). The path is
972            // the socket path, e.g. `unix:///run/sirup.sock`.
973            scheme if scheme.eq_ignore_ascii_case("unix") => {
974                (StreamStd::connect_unix(url.path())?, false)
975            }
976            scheme => {
977                let url = url.to_string();
978                let scheme = scheme.to_string();
979                return Err(ImapClientStdError::UrlUnsupportedScheme(url, scheme));
980            }
981        };
982
983        if starttls && is_tls {
984            return Err(ImapClientStdError::StartTlsOverTls);
985        }
986
987        // NOTE: STARTTLS needs the concrete StreamStd for upgrade_tls,
988        // so run it inline before boxing the stream.
989        let stream = if starttls {
990            let mut stream = stream;
991            let mut fragmentizer = Fragmentizer::new(FRAGMENTIZER_MAX_MESSAGE_SIZE);
992            run_starttls(&mut stream, &mut fragmentizer)?;
993            stream.upgrade_tls(tls)?
994        } else {
995            stream
996        };
997
998        // NOTE: 5s per-read timeout lets watch_mailbox poll shutdown
999        // during a silent IDLE; long FETCHes are unaffected.
1000        stream.set_read_timeout(Some(Duration::from_secs(5)))?;
1001
1002        let mut client = Self::new(stream);
1003        client.auto_id = auto_id;
1004
1005        let (mut capability, pre_authenticated) = if starttls {
1006            (client.capability()?, false)
1007        } else {
1008            let greeting = client.run(ImapGreetingGet::new(ImapGreetingGetOptions {
1009                ensure_capabilities: true,
1010            }))?;
1011            (greeting.capability, greeting.pre_authenticated)
1012        };
1013        client.pre_authenticated = pre_authenticated;
1014
1015        // NOTE: a PREAUTH greeting means the session opened already
1016        // authenticated (a sirup-style proxy), so the SASL step is
1017        // skipped even when credentials are configured.
1018        if let Some(sasl) = sasl.map(Into::into).filter(|_| !pre_authenticated) {
1019            let ir = capability.contains(&Capability::SaslIr);
1020
1021            capability = match sasl {
1022                Sasl::Anonymous(SaslAnonymous { message }) => {
1023                    let opts = ImapAuthAnonymousOptions {
1024                        initial_request: ir,
1025                        ensure_capabilities: true,
1026                        auto_id: client.auto_id.take(),
1027                    };
1028
1029                    client.auth_anonymous(message, opts)?
1030                }
1031                Sasl::Login(SaslLogin { username, password }) => {
1032                    let opts = ImapLoginOptions {
1033                        ensure_capabilities: true,
1034                        auto_id: client.auto_id.take(),
1035                    };
1036
1037                    client.login(username, password.expose_secret(), opts)?
1038                }
1039                Sasl::Plain(SaslPlain {
1040                    authzid,
1041                    authcid,
1042                    passwd,
1043                }) => {
1044                    let opts = ImapAuthPlainOptions {
1045                        initial_request: ir,
1046                        ensure_capabilities: true,
1047                        auto_id: client.auto_id.take(),
1048                    };
1049
1050                    client.auth_plain(authzid, authcid, passwd.expose_secret(), opts)?
1051                }
1052                Sasl::Oauthbearer(SaslOauthbearer {
1053                    username,
1054                    host,
1055                    port,
1056                    token,
1057                }) => {
1058                    let opts = ImapAuthOauthbearerOptions {
1059                        initial_request: ir,
1060                        ensure_capabilities: true,
1061                        auto_id: client.auto_id.take(),
1062                    };
1063
1064                    client.auth_oauthbearer(username, host, port, token.expose_secret(), opts)?
1065                }
1066                Sasl::Xoauth2(SaslXoauth2 { username, token }) => {
1067                    let opts = ImapAuthXoauth2Options {
1068                        initial_request: ir,
1069                        ensure_capabilities: true,
1070                        auto_id: client.auto_id.take(),
1071                    };
1072
1073                    client.auth_xoauth2(username, token.expose_secret(), opts)?
1074                }
1075                #[cfg(feature = "scram")]
1076                Sasl::ScramSha256(SaslScramSha256 { username, password }) => {
1077                    let opts = ImapAuthScramSha256Options {
1078                        initial_request: ir,
1079                        ensure_capabilities: true,
1080                        auto_id: client.auto_id.take(),
1081                    };
1082
1083                    client.auth_scram_sha256(username, password.expose_secret(), opts)?
1084                }
1085                #[cfg(not(feature = "scram"))]
1086                Sasl::ScramSha256(_) => {
1087                    return Err(ImapClientStdError::ScramSha256NotEnabled);
1088                }
1089            };
1090        }
1091
1092        Ok((client, capability))
1093    }
1094}
1095
1096/// Extracts the host from a TCP-bound IMAP URL (`imap`/`imaps`), erroring
1097/// when it carries none. The `unix` scheme does not go through here.
1098fn tcp_host(url: &Url) -> Result<&str, ImapClientStdError> {
1099    url.host_str()
1100        .ok_or_else(|| ImapClientStdError::UrlMissingHost(url.to_string()))
1101}
1102
1103/// Inline STARTTLS loop: keeps the concrete `StreamStd` so that
1104/// `upgrade_tls` can swap the underlying socket afterwards.
1105#[cfg(any(
1106    feature = "rustls-aws",
1107    feature = "rustls-ring",
1108    feature = "native-tls"
1109))]
1110fn run_starttls(
1111    stream: &mut StreamStd,
1112    fragmentizer: &mut Fragmentizer,
1113) -> Result<(), ImapClientStdError> {
1114    let mut coroutine = ImapStartTls::new();
1115    let mut buf = [0u8; READ_BUFFER_SIZE];
1116    let mut arg: Option<&[u8]> = None;
1117
1118    loop {
1119        match coroutine.resume(fragmentizer, arg.take()) {
1120            ImapCoroutineState::Complete(Ok(_)) => return Ok(()),
1121            ImapCoroutineState::Complete(Err(err)) => return Err(err.into()),
1122            ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
1123                let n = stream.read(&mut buf)?;
1124                arg = Some(&buf[..n]);
1125            }
1126            ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
1127                stream.write_all(&bytes)?;
1128            }
1129        }
1130    }
1131}
1132
1133/// Blocking stream the client runs over, auto-implemented for any
1134/// `Read + Write + Send + 'static`.
1135///
1136/// `as_any_mut` supports downcasting back to the concrete stream when
1137/// needed (e.g. for `set_read_timeout`).
1138pub trait ImapStream: Read + Write + Send + Any {
1139    /// The stream as a mutable `Any`, ready for downcasting.
1140    fn as_any_mut(&mut self) -> &mut dyn Any;
1141}
1142
1143impl<T: Read + Write + Send + Any> ImapStream for T {
1144    fn as_any_mut(&mut self) -> &mut dyn Any {
1145        self
1146    }
1147}