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::login_param::{ConnectionCandidate, ConnectionSecurity};
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::{
17    connect_tcp_inner, connect_tls_inner, run_connection_attempts, update_connection_history,
18};
19use crate::tools::time;
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 [&'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(resolved_addr, host, strict_tls).await
129            }
130            ConnectionSecurity::Starttls => {
131                Client::connect_starttls(resolved_addr, host, strict_tls).await
132            }
133            ConnectionSecurity::Plain => Client::connect_insecure(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                    "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(addr: SocketAddr, hostname: &str, strict_tls: bool) -> Result<Self> {
205        let tls_stream = connect_tls_inner(addr, hostname, strict_tls, alpn(addr.port())).await?;
206        let buffered_stream = BufWriter::new(tls_stream);
207        let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
208        let mut client = Client::new(session_stream);
209        let _greeting = client
210            .read_response()
211            .await
212            .context("failed to read greeting")??;
213        Ok(client)
214    }
215
216    async fn connect_insecure(addr: SocketAddr) -> Result<Self> {
217        let tcp_stream = connect_tcp_inner(addr).await?;
218        let buffered_stream = BufWriter::new(tcp_stream);
219        let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
220        let mut client = Client::new(session_stream);
221        let _greeting = client
222            .read_response()
223            .await
224            .context("failed to read greeting")??;
225        Ok(client)
226    }
227
228    async fn connect_starttls(addr: SocketAddr, host: &str, strict_tls: bool) -> Result<Self> {
229        let tcp_stream = connect_tcp_inner(addr).await?;
230
231        // Run STARTTLS command and convert the client back into a stream.
232        let buffered_tcp_stream = BufWriter::new(tcp_stream);
233        let mut client = async_imap::Client::new(buffered_tcp_stream);
234        let _greeting = client
235            .read_response()
236            .await
237            .context("failed to read greeting")??;
238        client
239            .run_command_and_check_ok("STARTTLS", None)
240            .await
241            .context("STARTTLS command failed")?;
242        let buffered_tcp_stream = client.into_inner();
243        let tcp_stream = buffered_tcp_stream.into_inner();
244
245        let tls_stream = wrap_tls(strict_tls, host, &[], tcp_stream)
246            .await
247            .context("STARTTLS upgrade failed")?;
248
249        let buffered_stream = BufWriter::new(tls_stream);
250        let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
251        let client = Client::new(session_stream);
252        Ok(client)
253    }
254
255    async fn connect_secure_proxy(
256        context: &Context,
257        domain: &str,
258        port: u16,
259        strict_tls: bool,
260        proxy_config: ProxyConfig,
261    ) -> Result<Self> {
262        let proxy_stream = proxy_config
263            .connect(context, domain, port, strict_tls)
264            .await?;
265        let tls_stream = wrap_tls(strict_tls, domain, alpn(port), proxy_stream).await?;
266        let buffered_stream = BufWriter::new(tls_stream);
267        let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
268        let mut client = Client::new(session_stream);
269        let _greeting = client
270            .read_response()
271            .await
272            .context("failed to read greeting")??;
273        Ok(client)
274    }
275
276    async fn connect_insecure_proxy(
277        context: &Context,
278        domain: &str,
279        port: u16,
280        proxy_config: ProxyConfig,
281    ) -> Result<Self> {
282        let proxy_stream = proxy_config.connect(context, domain, port, false).await?;
283        let buffered_stream = BufWriter::new(proxy_stream);
284        let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
285        let mut client = Client::new(session_stream);
286        let _greeting = client
287            .read_response()
288            .await
289            .context("failed to read greeting")??;
290        Ok(client)
291    }
292
293    async fn connect_starttls_proxy(
294        context: &Context,
295        hostname: &str,
296        port: u16,
297        proxy_config: ProxyConfig,
298        strict_tls: bool,
299    ) -> Result<Self> {
300        let proxy_stream = proxy_config
301            .connect(context, hostname, port, strict_tls)
302            .await?;
303
304        // Run STARTTLS command and convert the client back into a stream.
305        let buffered_proxy_stream = BufWriter::new(proxy_stream);
306        let mut client = ImapClient::new(buffered_proxy_stream);
307        let _greeting = client
308            .read_response()
309            .await
310            .context("failed to read greeting")??;
311        client
312            .run_command_and_check_ok("STARTTLS", None)
313            .await
314            .context("STARTTLS command failed")?;
315        let buffered_proxy_stream = client.into_inner();
316        let proxy_stream = buffered_proxy_stream.into_inner();
317
318        let tls_stream = wrap_tls(strict_tls, hostname, &[], proxy_stream)
319            .await
320            .context("STARTTLS upgrade failed")?;
321        let buffered_stream = BufWriter::new(tls_stream);
322        let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
323        let client = Client::new(session_stream);
324        Ok(client)
325    }
326}