Skip to main content

io_imap/client/
connect.rs

1//! End-to-end connect for the std client, the half that needs a TLS
2//! provider.
3//!
4//! The module is gated once where it is declared, so nothing inside
5//! repeats the feature list. It holds [`ImapClientStd::connect`], the
6//! [`ImapStream`] impl for the transport pimalaya-stream supplies, and
7//! the one decision the coroutines cannot make for themselves: drawing
8//! a SCRAM client nonce when the caller supplied none.
9
10use core::{any::Any, time::Duration};
11
12use alloc::vec::Vec;
13
14use std::io::{self, Read, Write};
15
16use imap_codec::{fragmentizer::Fragmentizer, imap_types::response::Capability};
17use io_sasl::mechanism::Sasl;
18#[cfg(feature = "scram")]
19use io_sasl::rfc5802::SaslScramCreds;
20use pimalaya_stream::{
21    retry::Retry,
22    stream::{Stream, TcpConnectOptions, TlsConnectOptions, UnixConnectOptions},
23    tls::Tls,
24};
25#[cfg(feature = "scram")]
26use rand::{RngExt, distr::Alphanumeric};
27use url::Url;
28
29use crate::{
30    client::{
31        FRAGMENTIZER_MAX_MESSAGE_SIZE, ImapClientError, ImapClientStd, ImapStream, READ_BUFFER_SIZE,
32    },
33    coroutine::*,
34    session::*,
35};
36
37impl ImapStream for Stream {
38    fn as_any_mut(&mut self) -> &mut dyn Any {
39        self
40    }
41
42    fn set_read_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
43        Stream::set_read_timeout(self, timeout)
44    }
45
46    fn stop_retrying(&mut self) {
47        self.retry = Retry::Never;
48    }
49}
50
51impl ImapClientStd {
52    /// End-to-end connect: TCP/TLS, optional STARTTLS, greeting,
53    /// optional SASL.
54    ///
55    /// `imap://` is plain TCP (143), `imaps://` is implicit TLS (993),
56    /// `unix://` is a local socket. `opts.starttls = true` is only valid
57    /// on a cleartext transport. Pass `None` as `sasl` to skip auth.
58    ///
59    /// SCRAM credentials carrying an empty nonce are given one drawn
60    /// here, an empty nonce being no nonce at all as far as RFC 5802 is
61    /// concerned; a caller wanting its own passes it in the credentials.
62    ///
63    /// Every protocol decision belongs to [`ImapSessionOpen`]; this
64    /// method only answers its transport requests with [`Stream`]. A
65    /// caller on another runtime pumps the same coroutine with its own
66    /// sockets.
67    pub fn connect(
68        url: &Url,
69        tls: &Tls,
70        sasl: Option<impl Into<Sasl>>,
71        opts: ImapSessionOpenOptions,
72    ) -> Result<(Self, Vec<Capability<'static>>), ImapClientError> {
73        let transport = ImapSessionTransport::from_url(url)?;
74        let sasl = sasl.map(Into::into).map(with_client_nonce);
75        let mut session = ImapSessionOpen::new(transport, sasl, opts);
76        let mut fragmentizer = Fragmentizer::new(FRAGMENTIZER_MAX_MESSAGE_SIZE);
77        let mut stream: Option<Stream> = None;
78        let mut buf = [0u8; READ_BUFFER_SIZE];
79        let mut arg: Option<&[u8]> = None;
80
81        // NOTE: the state machine always asks for a connect before any
82        // read, write or upgrade, so the stream is open by the time
83        // those arrive.
84        let missing = || io::Error::other("IMAP session yielded I/O before connecting");
85
86        loop {
87            match session.resume(&mut fragmentizer, arg.take()) {
88                ImapCoroutineState::Complete(Err(err)) => return Err(err.into()),
89                ImapCoroutineState::Complete(Ok(data)) => {
90                    let stream = stream.ok_or_else(missing)?;
91                    let mut client = Self::new(stream);
92                    client.fragmentizer = fragmentizer;
93                    client.pre_authenticated = data.pre_authenticated;
94                    return Ok((client, data.capability));
95                }
96                ImapCoroutineState::Yielded(ImapSessionOpenYield::WantsTcpConnect {
97                    host,
98                    port,
99                }) => {
100                    let opts = TcpConnectOptions::default();
101                    stream = Some(Stream::connect_tcp(host, port, opts)?);
102                }
103                ImapCoroutineState::Yielded(ImapSessionOpenYield::WantsTlsConnect {
104                    host,
105                    port,
106                }) => {
107                    let opts = TlsConnectOptions {
108                        tls: tls.clone(),
109                        ..Default::default()
110                    };
111
112                    stream = Some(Stream::connect_tls(host, port, opts)?);
113                }
114                ImapCoroutineState::Yielded(ImapSessionOpenYield::WantsUnixConnect(path)) => {
115                    let opts = UnixConnectOptions::default();
116                    stream = Some(Stream::connect_unix(path, opts)?);
117                }
118                ImapCoroutineState::Yielded(ImapSessionOpenYield::WantsTlsUpgrade) => {
119                    let plain = stream.take().ok_or_else(missing)?;
120                    stream = Some(plain.upgrade_tls(tls)?);
121                }
122                ImapCoroutineState::Yielded(ImapSessionOpenYield::WantsRead) => {
123                    let stream = stream.as_mut().ok_or_else(missing)?;
124                    let n = match stream.read(&mut buf)? {
125                        0 => {
126                            let kind = io::ErrorKind::UnexpectedEof;
127                            let err = "IMAP server closed the connection";
128                            return Err(io::Error::new(kind, err).into());
129                        }
130                        n => n,
131                    };
132
133                    arg = Some(&buf[..n]);
134                }
135                ImapCoroutineState::Yielded(ImapSessionOpenYield::WantsWrite(bytes)) => {
136                    let stream = stream.as_mut().ok_or_else(missing)?;
137                    stream.write_all(&bytes)?;
138                }
139            }
140        }
141    }
142}
143
144/// Draws the SCRAM-SHA-256 client nonce a caller left empty.
145///
146/// RFC 5802 asks for printable ASCII without commas, hence the
147/// alphanumeric sample, and at least 18 bytes of randomness. The
148/// coroutines take the nonce as an input so they stay free of
149/// randomness; this is where the std client makes that decision.
150#[cfg(feature = "scram")]
151fn with_client_nonce(sasl: Sasl) -> Sasl {
152    match sasl {
153        Sasl::ScramSha256(creds) if creds.nonce.is_empty() => {
154            let nonce = rand::rng().sample_iter(Alphanumeric).take(24).collect();
155            Sasl::ScramSha256(SaslScramCreds { nonce, ..creds })
156        }
157        sasl => sasl,
158    }
159}
160
161/// Stands in when the scram feature is off: no mechanism reads a nonce.
162#[cfg(not(feature = "scram"))]
163fn with_client_nonce(sasl: Sasl) -> Sasl {
164    sasl
165}