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 [&'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(strict_tls, hostname, alpn(addr.port()), logging_stream).await?;
214        let buffered_stream = BufWriter::new(tls_stream);
215        let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
216        let mut client = Client::new(session_stream);
217        let _greeting = client
218            .read_response()
219            .await?
220            .context("Failed to read greeting")?;
221        Ok(client)
222    }
223
224    async fn connect_insecure(context: &Context, addr: SocketAddr) -> Result<Self> {
225        let tcp_stream = connect_tcp_inner(addr).await?;
226        let account_id = context.get_id();
227        let events = context.events.clone();
228        let logging_stream = LoggingStream::new(tcp_stream, account_id, events)?;
229        let buffered_stream = BufWriter::new(logging_stream);
230        let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
231        let mut client = Client::new(session_stream);
232        let _greeting = client
233            .read_response()
234            .await?
235            .context("Failed to read greeting")?;
236        Ok(client)
237    }
238
239    async fn connect_starttls(
240        context: &Context,
241        addr: SocketAddr,
242        host: &str,
243        strict_tls: bool,
244    ) -> Result<Self> {
245        let tcp_stream = connect_tcp_inner(addr).await?;
246
247        let account_id = context.get_id();
248        let events = context.events.clone();
249        let tcp_stream = LoggingStream::new(tcp_stream, account_id, events)?;
250
251        // Run STARTTLS command and convert the client back into a stream.
252        let buffered_tcp_stream = BufWriter::new(tcp_stream);
253        let mut client = async_imap::Client::new(buffered_tcp_stream);
254        let _greeting = client
255            .read_response()
256            .await?
257            .context("Failed to read greeting")?;
258        client
259            .run_command_and_check_ok("STARTTLS", None)
260            .await
261            .context("STARTTLS command failed")?;
262        let buffered_tcp_stream = client.into_inner();
263        let tcp_stream = buffered_tcp_stream.into_inner();
264
265        let tls_stream = wrap_tls(strict_tls, host, &[], tcp_stream)
266            .await
267            .context("STARTTLS upgrade failed")?;
268        let buffered_stream = BufWriter::new(tls_stream);
269        let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
270        let client = Client::new(session_stream);
271        Ok(client)
272    }
273
274    async fn connect_secure_proxy(
275        context: &Context,
276        domain: &str,
277        port: u16,
278        strict_tls: bool,
279        proxy_config: ProxyConfig,
280    ) -> Result<Self> {
281        let proxy_stream = proxy_config
282            .connect(context, domain, port, strict_tls)
283            .await?;
284        let tls_stream = wrap_tls(strict_tls, domain, alpn(port), proxy_stream).await?;
285        let buffered_stream = BufWriter::new(tls_stream);
286        let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
287        let mut client = Client::new(session_stream);
288        let _greeting = client
289            .read_response()
290            .await?
291            .context("Failed to read greeting")?;
292        Ok(client)
293    }
294
295    async fn connect_insecure_proxy(
296        context: &Context,
297        domain: &str,
298        port: u16,
299        proxy_config: ProxyConfig,
300    ) -> Result<Self> {
301        let proxy_stream = proxy_config.connect(context, domain, port, false).await?;
302        let buffered_stream = BufWriter::new(proxy_stream);
303        let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
304        let mut client = Client::new(session_stream);
305        let _greeting = client
306            .read_response()
307            .await?
308            .context("Failed to read greeting")?;
309        Ok(client)
310    }
311
312    async fn connect_starttls_proxy(
313        context: &Context,
314        hostname: &str,
315        port: u16,
316        proxy_config: ProxyConfig,
317        strict_tls: bool,
318    ) -> Result<Self> {
319        let proxy_stream = proxy_config
320            .connect(context, hostname, port, strict_tls)
321            .await?;
322
323        // Run STARTTLS command and convert the client back into a stream.
324        let buffered_proxy_stream = BufWriter::new(proxy_stream);
325        let mut client = ImapClient::new(buffered_proxy_stream);
326        let _greeting = client
327            .read_response()
328            .await?
329            .context("Failed to read greeting")?;
330        client
331            .run_command_and_check_ok("STARTTLS", None)
332            .await
333            .context("STARTTLS command failed")?;
334        let buffered_proxy_stream = client.into_inner();
335        let proxy_stream = buffered_proxy_stream.into_inner();
336
337        let tls_stream = wrap_tls(strict_tls, hostname, &[], proxy_stream)
338            .await
339            .context("STARTTLS upgrade failed")?;
340        let buffered_stream = BufWriter::new(tls_stream);
341        let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
342        let client = Client::new(session_stream);
343        Ok(client)
344    }
345}