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, info, warn};
12use crate::login_param::{ConnectionCandidate, ConnectionSecurity};
13use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp};
14use crate::net::proxy::ProxyConfig;
15use crate::net::session::SessionStream;
16use crate::net::tls::wrap_tls;
17use crate::net::{connect_tcp_inner, run_connection_attempts, update_connection_history};
18use crate::tools::time;
19
20#[derive(Debug)]
21pub(crate) struct Client {
22    inner: ImapClient<Box<dyn SessionStream>>,
23}
24
25impl Deref for Client {
26    type Target = ImapClient<Box<dyn SessionStream>>;
27
28    fn deref(&self) -> &Self::Target {
29        &self.inner
30    }
31}
32
33impl DerefMut for Client {
34    fn deref_mut(&mut self) -> &mut Self::Target {
35        &mut self.inner
36    }
37}
38
39/// Converts port number to ALPN list.
40fn alpn(port: u16) -> &'static str {
41    if port == 993 {
42        // Do not request ALPN on standard port.
43        ""
44    } else {
45        "imap"
46    }
47}
48
49/// Determine server capabilities.
50///
51/// If server supports ID capability, send our client ID.
52pub(crate) async fn determine_capabilities(
53    session: &mut ImapSession<Box<dyn SessionStream>>,
54) -> Result<Capabilities> {
55    let caps = session
56        .capabilities()
57        .await
58        .context("CAPABILITY command error")?;
59    let server_id = if caps.has_str("ID") {
60        session.id([("name", Some("Delta Chat"))]).await?
61    } else {
62        None
63    };
64    let capabilities = Capabilities {
65        can_idle: caps.has_str("IDLE"),
66        can_move: caps.has_str("MOVE"),
67        can_check_quota: caps.has_str("QUOTA"),
68        can_condstore: caps.has_str("CONDSTORE"),
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                    "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 tcp_stream = connect_tcp_inner(addr).await?;
210        let account_id = context.get_id();
211        let events = context.events.clone();
212        let logging_stream = LoggingStream::new(tcp_stream, account_id, events)?;
213        let tls_stream = wrap_tls(
214            strict_tls,
215            hostname,
216            addr.port(),
217            alpn(addr.port()),
218            logging_stream,
219            &context.tls_session_store,
220        )
221        .await?;
222        let buffered_stream = BufWriter::new(tls_stream);
223        let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
224        let mut client = Client::new(session_stream);
225        let _greeting = client
226            .read_response()
227            .await?
228            .context("Failed to read greeting")?;
229        Ok(client)
230    }
231
232    async fn connect_insecure(context: &Context, addr: SocketAddr) -> Result<Self> {
233        let tcp_stream = connect_tcp_inner(addr).await?;
234        let account_id = context.get_id();
235        let events = context.events.clone();
236        let logging_stream = LoggingStream::new(tcp_stream, account_id, events)?;
237        let buffered_stream = BufWriter::new(logging_stream);
238        let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
239        let mut client = Client::new(session_stream);
240        let _greeting = client
241            .read_response()
242            .await?
243            .context("Failed to read greeting")?;
244        Ok(client)
245    }
246
247    async fn connect_starttls(
248        context: &Context,
249        addr: SocketAddr,
250        host: &str,
251        strict_tls: bool,
252    ) -> Result<Self> {
253        let tcp_stream = connect_tcp_inner(addr).await?;
254
255        let account_id = context.get_id();
256        let events = context.events.clone();
257        let tcp_stream = LoggingStream::new(tcp_stream, account_id, events)?;
258
259        // Run STARTTLS command and convert the client back into a stream.
260        let buffered_tcp_stream = BufWriter::new(tcp_stream);
261        let mut client = async_imap::Client::new(buffered_tcp_stream);
262        let _greeting = client
263            .read_response()
264            .await?
265            .context("Failed to read greeting")?;
266        client
267            .run_command_and_check_ok("STARTTLS", None)
268            .await
269            .context("STARTTLS command failed")?;
270        let buffered_tcp_stream = client.into_inner();
271        let tcp_stream = buffered_tcp_stream.into_inner();
272
273        let tls_stream = wrap_tls(
274            strict_tls,
275            host,
276            addr.port(),
277            "",
278            tcp_stream,
279            &context.tls_session_store,
280        )
281        .await
282        .context("STARTTLS upgrade failed")?;
283        let buffered_stream = BufWriter::new(tls_stream);
284        let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
285        let client = Client::new(session_stream);
286        Ok(client)
287    }
288
289    async fn connect_secure_proxy(
290        context: &Context,
291        domain: &str,
292        port: u16,
293        strict_tls: bool,
294        proxy_config: ProxyConfig,
295    ) -> Result<Self> {
296        let proxy_stream = proxy_config
297            .connect(context, domain, port, strict_tls)
298            .await?;
299        let tls_stream = wrap_tls(
300            strict_tls,
301            domain,
302            port,
303            alpn(port),
304            proxy_stream,
305            &context.tls_session_store,
306        )
307        .await?;
308        let buffered_stream = BufWriter::new(tls_stream);
309        let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
310        let mut client = Client::new(session_stream);
311        let _greeting = client
312            .read_response()
313            .await?
314            .context("Failed to read greeting")?;
315        Ok(client)
316    }
317
318    async fn connect_insecure_proxy(
319        context: &Context,
320        domain: &str,
321        port: u16,
322        proxy_config: ProxyConfig,
323    ) -> Result<Self> {
324        let proxy_stream = proxy_config.connect(context, domain, port, false).await?;
325        let buffered_stream = BufWriter::new(proxy_stream);
326        let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
327        let mut client = Client::new(session_stream);
328        let _greeting = client
329            .read_response()
330            .await?
331            .context("Failed to read greeting")?;
332        Ok(client)
333    }
334
335    async fn connect_starttls_proxy(
336        context: &Context,
337        hostname: &str,
338        port: u16,
339        proxy_config: ProxyConfig,
340        strict_tls: bool,
341    ) -> Result<Self> {
342        let proxy_stream = proxy_config
343            .connect(context, hostname, port, strict_tls)
344            .await?;
345
346        // Run STARTTLS command and convert the client back into a stream.
347        let buffered_proxy_stream = BufWriter::new(proxy_stream);
348        let mut client = ImapClient::new(buffered_proxy_stream);
349        let _greeting = client
350            .read_response()
351            .await?
352            .context("Failed to read greeting")?;
353        client
354            .run_command_and_check_ok("STARTTLS", None)
355            .await
356            .context("STARTTLS command failed")?;
357        let buffered_proxy_stream = client.into_inner();
358        let proxy_stream = buffered_proxy_stream.into_inner();
359
360        let tls_stream = wrap_tls(
361            strict_tls,
362            hostname,
363            port,
364            "",
365            proxy_stream,
366            &context.tls_session_store,
367        )
368        .await
369        .context("STARTTLS upgrade failed")?;
370        let buffered_stream = BufWriter::new(tls_stream);
371        let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
372        let client = Client::new(session_stream);
373        Ok(client)
374    }
375}