deltachat/imap/
client.rs

1use std::net::SocketAddr;
2use std::ops::{Deref, DerefMut};
3
4use anyhow::{Context as _, Result};
5use async_imap::Client as ImapClient;
6use async_imap::Session as ImapSession;
7use tokio::io::BufWriter;
8
9use super::capabilities::Capabilities;
10use crate::context::Context;
11use crate::log::{LoggingStream, warn};
12use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp};
13use crate::net::proxy::ProxyConfig;
14use crate::net::session::SessionStream;
15use crate::net::tls::wrap_tls;
16use crate::net::{connect_tcp_inner, run_connection_attempts, update_connection_history};
17use crate::tools::time;
18use crate::transport::ConnectionCandidate;
19use crate::transport::ConnectionSecurity;
20
21#[derive(Debug)]
22pub(crate) struct Client {
23    inner: ImapClient<Box<dyn SessionStream>>,
24}
25
26impl Deref for Client {
27    type Target = ImapClient<Box<dyn SessionStream>>;
28
29    fn deref(&self) -> &Self::Target {
30        &self.inner
31    }
32}
33
34impl DerefMut for Client {
35    fn deref_mut(&mut self) -> &mut Self::Target {
36        &mut self.inner
37    }
38}
39
40/// Converts port number to ALPN list.
41fn alpn(port: u16) -> &'static str {
42    if port == 993 {
43        // Do not request ALPN on standard port.
44        ""
45    } else {
46        "imap"
47    }
48}
49
50/// Determine server capabilities.
51///
52/// If server supports ID capability, send our client ID.
53pub(crate) async fn determine_capabilities(
54    session: &mut ImapSession<Box<dyn SessionStream>>,
55) -> Result<Capabilities> {
56    let caps = session
57        .capabilities()
58        .await
59        .context("CAPABILITY command error")?;
60    let server_id = if caps.has_str("ID") {
61        session.id([("name", Some("Delta Chat"))]).await?
62    } else {
63        None
64    };
65    let capabilities = Capabilities {
66        can_idle: caps.has_str("IDLE"),
67        can_move: caps.has_str("MOVE"),
68        can_check_quota: caps.has_str("QUOTA"),
69        can_condstore: caps.has_str("CONDSTORE"),
70        can_metadata: caps.has_str("METADATA"),
71        can_compress: caps.has_str("COMPRESS=DEFLATE"),
72        can_push: caps.has_str("XDELTAPUSH"),
73        is_chatmail: caps.has_str("XCHATMAIL"),
74        server_id,
75    };
76    Ok(capabilities)
77}
78
79impl Client {
80    fn new(stream: Box<dyn SessionStream>) -> Self {
81        Self {
82            inner: ImapClient::new(stream),
83        }
84    }
85
86    pub(crate) async fn login(
87        self,
88        username: &str,
89        password: &str,
90    ) -> Result<ImapSession<Box<dyn SessionStream>>> {
91        let Client { inner, .. } = self;
92
93        let session = inner
94            .login(username, password)
95            .await
96            .map_err(|(err, _client)| err)?;
97        Ok(session)
98    }
99
100    pub(crate) async fn authenticate(
101        self,
102        auth_type: &str,
103        authenticator: impl async_imap::Authenticator,
104    ) -> Result<ImapSession<Box<dyn SessionStream>>> {
105        let Client { inner, .. } = self;
106        let session = inner
107            .authenticate(auth_type, authenticator)
108            .await
109            .map_err(|(err, _client)| err)?;
110        Ok(session)
111    }
112
113    async fn connection_attempt(
114        context: Context,
115        host: String,
116        security: ConnectionSecurity,
117        resolved_addr: SocketAddr,
118        strict_tls: bool,
119    ) -> Result<Self> {
120        let context = &context;
121        let host = &host;
122        info!(
123            context,
124            "Attempting IMAP connection to {host} ({resolved_addr})."
125        );
126        let res = match security {
127            ConnectionSecurity::Tls => {
128                Client::connect_secure(context, resolved_addr, host, strict_tls).await
129            }
130            ConnectionSecurity::Starttls => {
131                Client::connect_starttls(context, resolved_addr, host, strict_tls).await
132            }
133            ConnectionSecurity::Plain => Client::connect_insecure(context, resolved_addr).await,
134        };
135        match res {
136            Ok(client) => {
137                let ip_addr = resolved_addr.ip().to_string();
138                let port = resolved_addr.port();
139
140                let save_cache = match security {
141                    ConnectionSecurity::Tls | ConnectionSecurity::Starttls => strict_tls,
142                    ConnectionSecurity::Plain => false,
143                };
144                if save_cache {
145                    update_connect_timestamp(context, host, &ip_addr).await?;
146                }
147                update_connection_history(context, "imap", host, port, &ip_addr, time()).await?;
148                Ok(client)
149            }
150            Err(err) => {
151                warn!(
152                    context,
153                    "IMAP failed to connect to {host} ({resolved_addr}): {err:#}."
154                );
155                Err(err)
156            }
157        }
158    }
159
160    pub async fn connect(
161        context: &Context,
162        proxy_config: Option<ProxyConfig>,
163        strict_tls: bool,
164        candidate: &ConnectionCandidate,
165    ) -> Result<Self> {
166        let host = &candidate.host;
167        let port = candidate.port;
168        let security = candidate.security;
169        if let Some(proxy_config) = proxy_config {
170            let client = match security {
171                ConnectionSecurity::Tls => {
172                    Client::connect_secure_proxy(context, host, port, strict_tls, proxy_config)
173                        .await?
174                }
175                ConnectionSecurity::Starttls => {
176                    Client::connect_starttls_proxy(context, host, port, proxy_config, strict_tls)
177                        .await?
178                }
179                ConnectionSecurity::Plain => {
180                    Client::connect_insecure_proxy(context, host, port, proxy_config).await?
181                }
182            };
183            update_connection_history(context, "imap", host, port, host, time()).await?;
184            Ok(client)
185        } else {
186            let load_cache = match security {
187                ConnectionSecurity::Tls | ConnectionSecurity::Starttls => strict_tls,
188                ConnectionSecurity::Plain => false,
189            };
190
191            let connection_futures =
192                lookup_host_with_cache(context, host, port, "imap", load_cache)
193                    .await?
194                    .into_iter()
195                    .map(|resolved_addr| {
196                        let context = context.clone();
197                        let host = host.to_string();
198                        Self::connection_attempt(context, host, security, resolved_addr, strict_tls)
199                    });
200            run_connection_attempts(connection_futures).await
201        }
202    }
203
204    async fn connect_secure(
205        context: &Context,
206        addr: SocketAddr,
207        hostname: &str,
208        strict_tls: bool,
209    ) -> Result<Self> {
210        let use_sni = true;
211        let tcp_stream = connect_tcp_inner(addr).await?;
212        let account_id = context.get_id();
213        let events = context.events.clone();
214        let logging_stream = LoggingStream::new(tcp_stream, account_id, events)?;
215        let tls_stream = wrap_tls(
216            strict_tls,
217            hostname,
218            addr.port(),
219            use_sni,
220            alpn(addr.port()),
221            logging_stream,
222            &context.tls_session_store,
223            &context.spki_hash_store,
224            &context.sql,
225        )
226        .await?;
227        let buffered_stream = BufWriter::new(tls_stream);
228        let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
229        let mut client = Client::new(session_stream);
230        let _greeting = client
231            .read_response()
232            .await?
233            .context("Failed to read greeting")?;
234        Ok(client)
235    }
236
237    async fn connect_insecure(context: &Context, addr: SocketAddr) -> Result<Self> {
238        let tcp_stream = connect_tcp_inner(addr).await?;
239        let account_id = context.get_id();
240        let events = context.events.clone();
241        let logging_stream = LoggingStream::new(tcp_stream, account_id, events)?;
242        let buffered_stream = BufWriter::new(logging_stream);
243        let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
244        let mut client = Client::new(session_stream);
245        let _greeting = client
246            .read_response()
247            .await?
248            .context("Failed to read greeting")?;
249        Ok(client)
250    }
251
252    async fn connect_starttls(
253        context: &Context,
254        addr: SocketAddr,
255        host: &str,
256        strict_tls: bool,
257    ) -> Result<Self> {
258        let use_sni = false;
259        let tcp_stream = connect_tcp_inner(addr).await?;
260
261        let account_id = context.get_id();
262        let events = context.events.clone();
263        let tcp_stream = LoggingStream::new(tcp_stream, account_id, events)?;
264
265        // Run STARTTLS command and convert the client back into a stream.
266        let buffered_tcp_stream = BufWriter::new(tcp_stream);
267        let mut client = async_imap::Client::new(buffered_tcp_stream);
268        let _greeting = client
269            .read_response()
270            .await?
271            .context("Failed to read greeting")?;
272        client
273            .run_command_and_check_ok("STARTTLS", None)
274            .await
275            .context("STARTTLS command failed")?;
276        let buffered_tcp_stream = client.into_inner();
277        let tcp_stream = buffered_tcp_stream.into_inner();
278
279        let tls_stream = wrap_tls(
280            strict_tls,
281            host,
282            addr.port(),
283            use_sni,
284            "",
285            tcp_stream,
286            &context.tls_session_store,
287            &context.spki_hash_store,
288            &context.sql,
289        )
290        .await
291        .context("STARTTLS upgrade failed")?;
292        let buffered_stream = BufWriter::new(tls_stream);
293        let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
294        let client = Client::new(session_stream);
295        Ok(client)
296    }
297
298    async fn connect_secure_proxy(
299        context: &Context,
300        domain: &str,
301        port: u16,
302        strict_tls: bool,
303        proxy_config: ProxyConfig,
304    ) -> Result<Self> {
305        let use_sni = true;
306        let proxy_stream = proxy_config
307            .connect(context, domain, port, strict_tls)
308            .await?;
309        let tls_stream = wrap_tls(
310            strict_tls,
311            domain,
312            port,
313            use_sni,
314            alpn(port),
315            proxy_stream,
316            &context.tls_session_store,
317            &context.spki_hash_store,
318            &context.sql,
319        )
320        .await?;
321        let buffered_stream = BufWriter::new(tls_stream);
322        let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
323        let mut client = Client::new(session_stream);
324        let _greeting = client
325            .read_response()
326            .await?
327            .context("Failed to read greeting")?;
328        Ok(client)
329    }
330
331    async fn connect_insecure_proxy(
332        context: &Context,
333        domain: &str,
334        port: u16,
335        proxy_config: ProxyConfig,
336    ) -> Result<Self> {
337        let proxy_stream = proxy_config.connect(context, domain, port, false).await?;
338        let buffered_stream = BufWriter::new(proxy_stream);
339        let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
340        let mut client = Client::new(session_stream);
341        let _greeting = client
342            .read_response()
343            .await?
344            .context("Failed to read greeting")?;
345        Ok(client)
346    }
347
348    async fn connect_starttls_proxy(
349        context: &Context,
350        hostname: &str,
351        port: u16,
352        proxy_config: ProxyConfig,
353        strict_tls: bool,
354    ) -> Result<Self> {
355        let use_sni = false;
356        let proxy_stream = proxy_config
357            .connect(context, hostname, port, strict_tls)
358            .await?;
359
360        // Run STARTTLS command and convert the client back into a stream.
361        let buffered_proxy_stream = BufWriter::new(proxy_stream);
362        let mut client = ImapClient::new(buffered_proxy_stream);
363        let _greeting = client
364            .read_response()
365            .await?
366            .context("Failed to read greeting")?;
367        client
368            .run_command_and_check_ok("STARTTLS", None)
369            .await
370            .context("STARTTLS command failed")?;
371        let buffered_proxy_stream = client.into_inner();
372        let proxy_stream = buffered_proxy_stream.into_inner();
373
374        let tls_stream = wrap_tls(
375            strict_tls,
376            hostname,
377            port,
378            use_sni,
379            "",
380            proxy_stream,
381            &context.tls_session_store,
382            &context.spki_hash_store,
383            &context.sql,
384        )
385        .await
386        .context("STARTTLS upgrade failed")?;
387        let buffered_stream = BufWriter::new(tls_stream);
388        let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
389        let client = Client::new(session_stream);
390        Ok(client)
391    }
392}