Skip to main content

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_metadata: caps.has_str("METADATA"),
70        can_compress: caps.has_str("COMPRESS=DEFLATE"),
71        can_push: caps.has_str("XDELTAPUSH"),
72        is_chatmail: caps.has_str("XCHATMAIL"),
73        server_id,
74    };
75    Ok(capabilities)
76}
77
78impl Client {
79    fn new(stream: Box<dyn SessionStream>) -> Self {
80        Self {
81            inner: ImapClient::new(stream),
82        }
83    }
84
85    pub(crate) async fn login(
86        self,
87        username: &str,
88        password: &str,
89    ) -> Result<ImapSession<Box<dyn SessionStream>>> {
90        let Client { inner, .. } = self;
91
92        let session = inner
93            .login(username, password)
94            .await
95            .map_err(|(err, _client)| err)?;
96        Ok(session)
97    }
98
99    pub(crate) async fn authenticate(
100        self,
101        auth_type: &str,
102        authenticator: impl async_imap::Authenticator,
103    ) -> Result<ImapSession<Box<dyn SessionStream>>> {
104        let Client { inner, .. } = self;
105        let session = inner
106            .authenticate(auth_type, authenticator)
107            .await
108            .map_err(|(err, _client)| err)?;
109        Ok(session)
110    }
111
112    async fn connection_attempt(
113        context: Context,
114        host: String,
115        security: ConnectionSecurity,
116        resolved_addr: SocketAddr,
117        strict_tls: bool,
118    ) -> Result<Self> {
119        let context = &context;
120        let host = &host;
121        info!(
122            context,
123            "Attempting IMAP connection to {host} ({resolved_addr})."
124        );
125        let res = match security {
126            ConnectionSecurity::Tls => {
127                Client::connect_secure(context, resolved_addr, host, strict_tls).await
128            }
129            ConnectionSecurity::Starttls => {
130                Client::connect_starttls(context, resolved_addr, host, strict_tls).await
131            }
132            ConnectionSecurity::Plain => Client::connect_insecure(context, resolved_addr).await,
133        };
134        match res {
135            Ok(client) => {
136                let ip_addr = resolved_addr.ip().to_string();
137                let port = resolved_addr.port();
138
139                let save_cache = match security {
140                    ConnectionSecurity::Tls | ConnectionSecurity::Starttls => strict_tls,
141                    ConnectionSecurity::Plain => false,
142                };
143                if save_cache {
144                    update_connect_timestamp(context, host, &ip_addr).await?;
145                }
146                update_connection_history(context, "imap", host, port, &ip_addr, time()).await?;
147                Ok(client)
148            }
149            Err(err) => {
150                warn!(
151                    context,
152                    "IMAP failed to connect to {host} ({resolved_addr}): {err:#}."
153                );
154                Err(err)
155            }
156        }
157    }
158
159    pub async fn connect(
160        context: &Context,
161        proxy_config: Option<ProxyConfig>,
162        strict_tls: bool,
163        candidate: &ConnectionCandidate,
164    ) -> Result<Self> {
165        let host = &candidate.host;
166        let port = candidate.port;
167        let security = candidate.security;
168        if let Some(proxy_config) = proxy_config {
169            let client = match security {
170                ConnectionSecurity::Tls => {
171                    Client::connect_secure_proxy(context, host, port, strict_tls, proxy_config)
172                        .await?
173                }
174                ConnectionSecurity::Starttls => {
175                    Client::connect_starttls_proxy(context, host, port, proxy_config, strict_tls)
176                        .await?
177                }
178                ConnectionSecurity::Plain => {
179                    Client::connect_insecure_proxy(context, host, port, proxy_config).await?
180                }
181            };
182            update_connection_history(context, "imap", host, port, host, time()).await?;
183            Ok(client)
184        } else {
185            let load_cache = match security {
186                ConnectionSecurity::Tls | ConnectionSecurity::Starttls => strict_tls,
187                ConnectionSecurity::Plain => false,
188            };
189
190            let connection_futures =
191                lookup_host_with_cache(context, host, port, "imap", load_cache)
192                    .await?
193                    .into_iter()
194                    .map(|resolved_addr| {
195                        let context = context.clone();
196                        let host = host.to_string();
197                        Self::connection_attempt(context, host, security, resolved_addr, strict_tls)
198                    });
199            run_connection_attempts(connection_futures).await
200        }
201    }
202
203    async fn connect_secure(
204        context: &Context,
205        addr: SocketAddr,
206        hostname: &str,
207        strict_tls: bool,
208    ) -> Result<Self> {
209        let use_sni = true;
210        let tcp_stream = connect_tcp_inner(addr).await?;
211        let account_id = context.get_id();
212        let events = context.events.clone();
213        let logging_stream = LoggingStream::new(tcp_stream, account_id, events)?;
214        let tls_stream = wrap_tls(
215            strict_tls,
216            hostname,
217            addr.port(),
218            use_sni,
219            alpn(addr.port()),
220            logging_stream,
221            &context.tls_session_store,
222            &context.spki_hash_store,
223            &context.sql,
224        )
225        .await?;
226        let buffered_stream = BufWriter::new(tls_stream);
227        let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
228        let mut client = Client::new(session_stream);
229        let _greeting = client
230            .read_response()
231            .await?
232            .context("Failed to read greeting")?;
233        Ok(client)
234    }
235
236    async fn connect_insecure(context: &Context, addr: SocketAddr) -> Result<Self> {
237        let tcp_stream = connect_tcp_inner(addr).await?;
238        let account_id = context.get_id();
239        let events = context.events.clone();
240        let logging_stream = LoggingStream::new(tcp_stream, account_id, events)?;
241        let buffered_stream = BufWriter::new(logging_stream);
242        let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
243        let mut client = Client::new(session_stream);
244        let _greeting = client
245            .read_response()
246            .await?
247            .context("Failed to read greeting")?;
248        Ok(client)
249    }
250
251    async fn connect_starttls(
252        context: &Context,
253        addr: SocketAddr,
254        host: &str,
255        strict_tls: bool,
256    ) -> Result<Self> {
257        let use_sni = false;
258        let tcp_stream = connect_tcp_inner(addr).await?;
259
260        let account_id = context.get_id();
261        let events = context.events.clone();
262        let tcp_stream = LoggingStream::new(tcp_stream, account_id, events)?;
263
264        // Run STARTTLS command and convert the client back into a stream.
265        let buffered_tcp_stream = BufWriter::new(tcp_stream);
266        let mut client = async_imap::Client::new(buffered_tcp_stream);
267        let _greeting = client
268            .read_response()
269            .await?
270            .context("Failed to read greeting")?;
271        client
272            .run_command_and_check_ok("STARTTLS", None)
273            .await
274            .context("STARTTLS command failed")?;
275        let buffered_tcp_stream = client.into_inner();
276        let tcp_stream = buffered_tcp_stream.into_inner();
277
278        let tls_stream = wrap_tls(
279            strict_tls,
280            host,
281            addr.port(),
282            use_sni,
283            "",
284            tcp_stream,
285            &context.tls_session_store,
286            &context.spki_hash_store,
287            &context.sql,
288        )
289        .await
290        .context("STARTTLS upgrade failed")?;
291        let buffered_stream = BufWriter::new(tls_stream);
292        let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
293        let client = Client::new(session_stream);
294        Ok(client)
295    }
296
297    async fn connect_secure_proxy(
298        context: &Context,
299        domain: &str,
300        port: u16,
301        strict_tls: bool,
302        proxy_config: ProxyConfig,
303    ) -> Result<Self> {
304        let use_sni = true;
305        let proxy_stream = proxy_config
306            .connect(context, domain, port, strict_tls)
307            .await?;
308        let tls_stream = wrap_tls(
309            strict_tls,
310            domain,
311            port,
312            use_sni,
313            alpn(port),
314            proxy_stream,
315            &context.tls_session_store,
316            &context.spki_hash_store,
317            &context.sql,
318        )
319        .await?;
320        let buffered_stream = BufWriter::new(tls_stream);
321        let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
322        let mut client = Client::new(session_stream);
323        let _greeting = client
324            .read_response()
325            .await?
326            .context("Failed to read greeting")?;
327        Ok(client)
328    }
329
330    async fn connect_insecure_proxy(
331        context: &Context,
332        domain: &str,
333        port: u16,
334        proxy_config: ProxyConfig,
335    ) -> Result<Self> {
336        let proxy_stream = proxy_config.connect(context, domain, port, false).await?;
337        let buffered_stream = BufWriter::new(proxy_stream);
338        let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
339        let mut client = Client::new(session_stream);
340        let _greeting = client
341            .read_response()
342            .await?
343            .context("Failed to read greeting")?;
344        Ok(client)
345    }
346
347    async fn connect_starttls_proxy(
348        context: &Context,
349        hostname: &str,
350        port: u16,
351        proxy_config: ProxyConfig,
352        strict_tls: bool,
353    ) -> Result<Self> {
354        let use_sni = false;
355        let proxy_stream = proxy_config
356            .connect(context, hostname, port, strict_tls)
357            .await?;
358
359        // Run STARTTLS command and convert the client back into a stream.
360        let buffered_proxy_stream = BufWriter::new(proxy_stream);
361        let mut client = ImapClient::new(buffered_proxy_stream);
362        let _greeting = client
363            .read_response()
364            .await?
365            .context("Failed to read greeting")?;
366        client
367            .run_command_and_check_ok("STARTTLS", None)
368            .await
369            .context("STARTTLS command failed")?;
370        let buffered_proxy_stream = client.into_inner();
371        let proxy_stream = buffered_proxy_stream.into_inner();
372
373        let tls_stream = wrap_tls(
374            strict_tls,
375            hostname,
376            port,
377            use_sni,
378            "",
379            proxy_stream,
380            &context.tls_session_store,
381            &context.spki_hash_store,
382            &context.sql,
383        )
384        .await
385        .context("STARTTLS upgrade failed")?;
386        let buffered_stream = BufWriter::new(tls_stream);
387        let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
388        let client = Client::new(session_stream);
389        Ok(client)
390    }
391}