use std::net::SocketAddr; use std::ops::{Deref, DerefMut}; use anyhow::{Context as _, Result}; use async_imap::Client as ImapClient; use async_imap::Session as ImapSession; use tokio::io::BufWriter; use super::capabilities::Capabilities; use crate::context::Context; use crate::log::{LoggingStream, warn}; use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp}; use crate::net::proxy::ProxyConfig; use crate::net::session::SessionStream; use crate::net::tls::wrap_tls; use crate::net::{connect_tcp_inner, run_connection_attempts, update_connection_history}; use crate::tools::time; use crate::transport::ConnectionCandidate; use crate::transport::ConnectionSecurity; #[derive(Debug)] pub(crate) struct Client { inner: ImapClient>, } impl Deref for Client { type Target = ImapClient>; fn deref(&self) -> &Self::Target { &self.inner } } impl DerefMut for Client { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inner } } /// Converts port number to ALPN list. fn alpn(port: u16) -> &'static str { if port == 993 { // Do not request ALPN on standard port. "" } else { "imap" } } /// Determine server capabilities. /// /// If server supports ID capability, send our client ID. pub(crate) async fn determine_capabilities( session: &mut ImapSession>, ) -> Result { let caps = session .capabilities() .await .context("CAPABILITY command error")?; let server_id = if caps.has_str("ID") { session.id([("name", Some("Delta Chat"))]).await? } else { None }; let capabilities = Capabilities { can_idle: caps.has_str("IDLE"), can_move: caps.has_str("MOVE"), can_check_quota: caps.has_str("QUOTA"), can_condstore: caps.has_str("CONDSTORE"), can_metadata: caps.has_str("METADATA"), can_compress: caps.has_str("COMPRESS=DEFLATE"), can_push: caps.has_str("XDELTAPUSH"), is_chatmail: caps.has_str("XCHATMAIL"), server_id, }; Ok(capabilities) } impl Client { fn new(stream: Box) -> Self { Self { inner: ImapClient::new(stream), } } pub(crate) async fn login( self, username: &str, password: &str, ) -> Result>> { let Client { inner, .. } = self; let session = inner .login(username, password) .await .map_err(|(err, _client)| err)?; Ok(session) } pub(crate) async fn authenticate( self, auth_type: &str, authenticator: impl async_imap::Authenticator, ) -> Result>> { let Client { inner, .. } = self; let session = inner .authenticate(auth_type, authenticator) .await .map_err(|(err, _client)| err)?; Ok(session) } async fn connection_attempt( context: Context, host: String, security: ConnectionSecurity, resolved_addr: SocketAddr, strict_tls: bool, ) -> Result { let context = &context; let host = &host; info!( context, "Attempting IMAP connection to {host} ({resolved_addr})." ); let res = match security { ConnectionSecurity::Tls => { Client::connect_secure(context, resolved_addr, host, strict_tls).await } ConnectionSecurity::Starttls => { Client::connect_starttls(context, resolved_addr, host, strict_tls).await } ConnectionSecurity::Plain => Client::connect_insecure(context, resolved_addr).await, }; match res { Ok(client) => { let ip_addr = resolved_addr.ip().to_string(); let port = resolved_addr.port(); let save_cache = match security { ConnectionSecurity::Tls | ConnectionSecurity::Starttls => strict_tls, ConnectionSecurity::Plain => false, }; if save_cache { update_connect_timestamp(context, host, &ip_addr).await?; } update_connection_history(context, "imap", host, port, &ip_addr, time()).await?; Ok(client) } Err(err) => { warn!( context, "Failed to connect to {host} ({resolved_addr}): {err:#}." ); Err(err) } } } pub async fn connect( context: &Context, proxy_config: Option, strict_tls: bool, candidate: ConnectionCandidate, ) -> Result { let host = &candidate.host; let port = candidate.port; let security = candidate.security; if let Some(proxy_config) = proxy_config { let client = match security { ConnectionSecurity::Tls => { Client::connect_secure_proxy(context, host, port, strict_tls, proxy_config) .await? } ConnectionSecurity::Starttls => { Client::connect_starttls_proxy(context, host, port, proxy_config, strict_tls) .await? } ConnectionSecurity::Plain => { Client::connect_insecure_proxy(context, host, port, proxy_config).await? } }; update_connection_history(context, "imap", host, port, host, time()).await?; Ok(client) } else { let load_cache = match security { ConnectionSecurity::Tls | ConnectionSecurity::Starttls => strict_tls, ConnectionSecurity::Plain => false, }; let connection_futures = lookup_host_with_cache(context, host, port, "imap", load_cache) .await? .into_iter() .map(|resolved_addr| { let context = context.clone(); let host = host.to_string(); Self::connection_attempt(context, host, security, resolved_addr, strict_tls) }); run_connection_attempts(connection_futures).await } } async fn connect_secure( context: &Context, addr: SocketAddr, hostname: &str, strict_tls: bool, ) -> Result { let use_sni = true; let tcp_stream = connect_tcp_inner(addr).await?; let account_id = context.get_id(); let events = context.events.clone(); let logging_stream = LoggingStream::new(tcp_stream, account_id, events)?; let tls_stream = wrap_tls( strict_tls, hostname, addr.port(), use_sni, alpn(addr.port()), logging_stream, &context.tls_session_store, ) .await?; let buffered_stream = BufWriter::new(tls_stream); let session_stream: Box = Box::new(buffered_stream); let mut client = Client::new(session_stream); let _greeting = client .read_response() .await? .context("Failed to read greeting")?; Ok(client) } async fn connect_insecure(context: &Context, addr: SocketAddr) -> Result { let tcp_stream = connect_tcp_inner(addr).await?; let account_id = context.get_id(); let events = context.events.clone(); let logging_stream = LoggingStream::new(tcp_stream, account_id, events)?; let buffered_stream = BufWriter::new(logging_stream); let session_stream: Box = Box::new(buffered_stream); let mut client = Client::new(session_stream); let _greeting = client .read_response() .await? .context("Failed to read greeting")?; Ok(client) } async fn connect_starttls( context: &Context, addr: SocketAddr, host: &str, strict_tls: bool, ) -> Result { let use_sni = false; let tcp_stream = connect_tcp_inner(addr).await?; let account_id = context.get_id(); let events = context.events.clone(); let tcp_stream = LoggingStream::new(tcp_stream, account_id, events)?; // Run STARTTLS command and convert the client back into a stream. let buffered_tcp_stream = BufWriter::new(tcp_stream); let mut client = async_imap::Client::new(buffered_tcp_stream); let _greeting = client .read_response() .await? .context("Failed to read greeting")?; client .run_command_and_check_ok("STARTTLS", None) .await .context("STARTTLS command failed")?; let buffered_tcp_stream = client.into_inner(); let tcp_stream = buffered_tcp_stream.into_inner(); let tls_stream = wrap_tls( strict_tls, host, addr.port(), use_sni, "", tcp_stream, &context.tls_session_store, ) .await .context("STARTTLS upgrade failed")?; let buffered_stream = BufWriter::new(tls_stream); let session_stream: Box = Box::new(buffered_stream); let client = Client::new(session_stream); Ok(client) } async fn connect_secure_proxy( context: &Context, domain: &str, port: u16, strict_tls: bool, proxy_config: ProxyConfig, ) -> Result { let use_sni = true; let proxy_stream = proxy_config .connect(context, domain, port, strict_tls) .await?; let tls_stream = wrap_tls( strict_tls, domain, port, use_sni, alpn(port), proxy_stream, &context.tls_session_store, ) .await?; let buffered_stream = BufWriter::new(tls_stream); let session_stream: Box = Box::new(buffered_stream); let mut client = Client::new(session_stream); let _greeting = client .read_response() .await? .context("Failed to read greeting")?; Ok(client) } async fn connect_insecure_proxy( context: &Context, domain: &str, port: u16, proxy_config: ProxyConfig, ) -> Result { let proxy_stream = proxy_config.connect(context, domain, port, false).await?; let buffered_stream = BufWriter::new(proxy_stream); let session_stream: Box = Box::new(buffered_stream); let mut client = Client::new(session_stream); let _greeting = client .read_response() .await? .context("Failed to read greeting")?; Ok(client) } async fn connect_starttls_proxy( context: &Context, hostname: &str, port: u16, proxy_config: ProxyConfig, strict_tls: bool, ) -> Result { let use_sni = false; let proxy_stream = proxy_config .connect(context, hostname, port, strict_tls) .await?; // Run STARTTLS command and convert the client back into a stream. let buffered_proxy_stream = BufWriter::new(proxy_stream); let mut client = ImapClient::new(buffered_proxy_stream); let _greeting = client .read_response() .await? .context("Failed to read greeting")?; client .run_command_and_check_ok("STARTTLS", None) .await .context("STARTTLS command failed")?; let buffered_proxy_stream = client.into_inner(); let proxy_stream = buffered_proxy_stream.into_inner(); let tls_stream = wrap_tls( strict_tls, hostname, port, use_sni, "", proxy_stream, &context.tls_session_store, ) .await .context("STARTTLS upgrade failed")?; let buffered_stream = BufWriter::new(tls_stream); let session_stream: Box = Box::new(buffered_stream); let client = Client::new(session_stream); Ok(client) } }