diff --git a/src/imap/client.rs b/src/imap/client.rs index b6b106c45..e371a49c2 100644 --- a/src/imap/client.rs +++ b/src/imap/client.rs @@ -94,7 +94,7 @@ impl Client { port: u16, strict_tls: bool, ) -> Result { - let tcp_stream = connect_tcp(context, hostname, port, IMAP_TIMEOUT).await?; + let tcp_stream = connect_tcp(context, hostname, port, IMAP_TIMEOUT, strict_tls).await?; let tls = build_tls(strict_tls); let tls_stream = tls.connect(hostname, tcp_stream).await?; let buffered_stream = BufWriter::new(tls_stream); @@ -110,7 +110,7 @@ impl Client { } pub async fn connect_insecure(context: &Context, hostname: &str, port: u16) -> Result { - let tcp_stream = connect_tcp(context, hostname, port, IMAP_TIMEOUT).await?; + let tcp_stream = connect_tcp(context, hostname, port, IMAP_TIMEOUT, false).await?; let buffered_stream = BufWriter::new(tcp_stream); let session_stream: Box = Box::new(buffered_stream); let mut client = ImapClient::new(session_stream); @@ -128,7 +128,7 @@ impl Client { port: u16, strict_tls: bool, ) -> Result { - let tcp_stream = connect_tcp(context, hostname, port, IMAP_TIMEOUT).await?; + let tcp_stream = connect_tcp(context, hostname, port, IMAP_TIMEOUT, strict_tls).await?; // Run STARTTLS command and convert the client back into a stream. let mut client = ImapClient::new(tcp_stream); @@ -163,7 +163,7 @@ impl Client { socks5_config: Socks5Config, ) -> Result { let socks5_stream = socks5_config - .connect(context, domain, port, IMAP_TIMEOUT) + .connect(context, domain, port, IMAP_TIMEOUT, strict_tls) .await?; let tls = build_tls(strict_tls); let tls_stream = tls.connect(domain, socks5_stream).await?; @@ -185,7 +185,7 @@ impl Client { socks5_config: Socks5Config, ) -> Result { let socks5_stream = socks5_config - .connect(context, domain, port, IMAP_TIMEOUT) + .connect(context, domain, port, IMAP_TIMEOUT, false) .await?; let buffered_stream = BufWriter::new(socks5_stream); let session_stream: Box = Box::new(buffered_stream); @@ -206,7 +206,7 @@ impl Client { strict_tls: bool, ) -> Result { let socks5_stream = socks5_config - .connect(context, hostname, port, IMAP_TIMEOUT) + .connect(context, hostname, port, IMAP_TIMEOUT, strict_tls) .await?; // Run STARTTLS command and convert the client back into a stream. diff --git a/src/net.rs b/src/net.rs index 2885c0523..d5b6a1ec3 100644 --- a/src/net.rs +++ b/src/net.rs @@ -1,6 +1,7 @@ ///! # Common network utilities. use std::net::SocketAddr; use std::pin::Pin; +use std::str::FromStr; use std::time::Duration; use anyhow::{Context as _, Result}; @@ -9,6 +10,7 @@ use tokio::time::timeout; use tokio_io_timeout::TimeoutStream; use crate::context::Context; +use crate::tools::time; async fn connect_tcp_inner(addr: SocketAddr, timeout_val: Duration) -> Result { let tcp_stream = timeout(timeout_val, TcpStream::connect(addr)) @@ -18,23 +20,98 @@ async fn connect_tcp_inner(addr: SocketAddr, timeout_val: Duration) -> Result Result> { + let now = time(); + let mut resolved_addrs: Vec = lookup_host((hostname, port)).await?.collect(); + + for (i, addr) in resolved_addrs.iter().enumerate() { + info!(context, "Resolved {}:{} into {}.", hostname, port, &addr); + + let i = i64::try_from(i).unwrap_or_default(); + + // Update the cache. + // + // Add sequence number to the timestamp, so addresses are ordered by timestamp in the same + // order as the resolver returns them. + context + .sql + .execute( + "INSERT INTO dns_cache + (hostname, port, address, timestamp) + VALUES (?, ?, ?, ?) + ON CONFLICT (hostname, port, address) + DO UPDATE SET timestamp=excluded.timestamp", + paramsv![hostname, port, addr.to_string(), now.saturating_add(i)], + ) + .await?; + } + + if load_cache { + for cached_address in context + .sql + .query_map( + "SELECT address + FROM dns_cache + WHERE hostname = ? + AND ? < timestamp + 30 * 24 * 3600 + ORDER BY timestamp DESC", + paramsv![hostname, now], + |row| { + let address: String = row.get(0)?; + Ok(address) + }, + |rows| { + rows.collect::, _>>() + .map_err(Into::into) + }, + ) + .await? + { + match SocketAddr::from_str(&cached_address) { + Ok(addr) => { + if !resolved_addrs.contains(&addr) { + resolved_addrs.push(addr); + } + } + Err(err) => { + warn!( + context, + "Failed to parse cached address {:?}: {:#}.", cached_address, err + ); + } + } + } + } + + Ok(resolved_addrs) +} + /// Returns a TCP connection stream with read/write timeouts set /// and Nagle's algorithm disabled with `TCP_NODELAY`. /// /// `TCP_NODELAY` ensures writing to the stream always results in immediate sending of the packet /// to the network, which is important to reduce the latency of interactive protocols such as IMAP. +/// +/// If `load_cache` is true, may use cached DNS results. +/// Use this only if the connection is going to be protected with TLS. pub(crate) async fn connect_tcp( context: &Context, host: &str, port: u16, timeout_val: Duration, + load_cache: bool, ) -> Result>>> { let mut tcp_stream = None; - for resolved_addr in lookup_host((host, port)).await? { - info!( - context, - "Resolved {}:{} into {}.", host, port, &resolved_addr - ); + + for resolved_addr in lookup_host_with_cache(context, host, port, load_cache).await? { match connect_tcp_inner(resolved_addr, timeout_val).await { Ok(stream) => { tcp_stream = Some(stream); @@ -48,6 +125,7 @@ pub(crate) async fn connect_tcp( } } } + let tcp_stream = tcp_stream.with_context(|| format!("failed to connect to {}:{}", host, port))?; diff --git a/src/socks.rs b/src/socks.rs index 52eb4fd61..58d7aa1db 100644 --- a/src/socks.rs +++ b/src/socks.rs @@ -56,14 +56,18 @@ impl Socks5Config { } } + /// If `load_dns_cache` is true, loads cached DNS resolution results. + /// Use this only if the connection is going to be protected with TLS checks. pub async fn connect( &self, context: &Context, target_host: &str, target_port: u16, timeout_val: Duration, + load_dns_cache: bool, ) -> Result>>>> { - let tcp_stream = connect_tcp(context, &self.host, self.port, timeout_val).await?; + let tcp_stream = + connect_tcp(context, &self.host, self.port, timeout_val, load_dns_cache).await?; let authentication_method = if let Some((username, password)) = self.user_password.as_ref() { diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index fde27051c..549689c6a 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -671,6 +671,19 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid); ) .await?; } + if dbversion < 97 { + sql.execute_migration( + "CREATE TABLE dns_cache ( + hostname TEXT NOT NULL, + port INTEGER NOT NULL, + address TEXT NOT NULL, + timestamp INTEGER NOT NULL, + UNIQUE (hostname, port, address) + )", + 97, + ) + .await?; + } let new_version = sql .get_raw_config_int(VERSION_CFG)