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