mirror of
https://github.com/chatmail/core.git
synced 2026-04-17 21:46:35 +03:00
This change introduces new config options `proxy_enabled` and `proxy_url` that replace `socks5_*`. Tested with deltachat-repl by starting it with `cargo run --locked -p deltachat-repl -- deltachat-db` and running ``` > set proxy_enabled 1 > set proxy_url ss://... > setqr dcaccount:https://chatmail.example.org/new > configure ```
323 lines
11 KiB
Rust
323 lines
11 KiB
Rust
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 super::session::Session;
|
|
use crate::context::Context;
|
|
use crate::login_param::{ConnectionCandidate, ConnectionSecurity};
|
|
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, connect_tls_inner, run_connection_attempts, update_connection_history,
|
|
};
|
|
use crate::tools::time;
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct Client {
|
|
inner: ImapClient<Box<dyn SessionStream>>,
|
|
}
|
|
|
|
impl Deref for Client {
|
|
type Target = ImapClient<Box<dyn SessionStream>>;
|
|
|
|
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 [&'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.
|
|
async fn determine_capabilities(
|
|
session: &mut ImapSession<Box<dyn SessionStream>>,
|
|
) -> Result<Capabilities> {
|
|
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_push: caps.has_str("XDELTAPUSH"),
|
|
is_chatmail: caps.has_str("XCHATMAIL"),
|
|
server_id,
|
|
};
|
|
Ok(capabilities)
|
|
}
|
|
|
|
impl Client {
|
|
fn new(stream: Box<dyn SessionStream>) -> Self {
|
|
Self {
|
|
inner: ImapClient::new(stream),
|
|
}
|
|
}
|
|
|
|
pub(crate) async fn login(self, username: &str, password: &str) -> Result<Session> {
|
|
let Client { inner, .. } = self;
|
|
let mut session = inner
|
|
.login(username, password)
|
|
.await
|
|
.map_err(|(err, _client)| err)?;
|
|
let capabilities = determine_capabilities(&mut session).await?;
|
|
Ok(Session::new(session, capabilities))
|
|
}
|
|
|
|
pub(crate) async fn authenticate(
|
|
self,
|
|
auth_type: &str,
|
|
authenticator: impl async_imap::Authenticator,
|
|
) -> Result<Session> {
|
|
let Client { inner, .. } = self;
|
|
let mut session = inner
|
|
.authenticate(auth_type, authenticator)
|
|
.await
|
|
.map_err(|(err, _client)| err)?;
|
|
let capabilities = determine_capabilities(&mut session).await?;
|
|
Ok(Session::new(session, capabilities))
|
|
}
|
|
|
|
async fn connection_attempt(
|
|
context: Context,
|
|
host: String,
|
|
security: ConnectionSecurity,
|
|
resolved_addr: SocketAddr,
|
|
strict_tls: bool,
|
|
) -> Result<Self> {
|
|
let context = &context;
|
|
let host = &host;
|
|
info!(
|
|
context,
|
|
"Attempting IMAP connection to {host} ({resolved_addr})."
|
|
);
|
|
let res = match security {
|
|
ConnectionSecurity::Tls => {
|
|
Client::connect_secure(resolved_addr, host, strict_tls).await
|
|
}
|
|
ConnectionSecurity::Starttls => {
|
|
Client::connect_starttls(resolved_addr, host, strict_tls).await
|
|
}
|
|
ConnectionSecurity::Plain => Client::connect_insecure(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<ProxyConfig>,
|
|
strict_tls: bool,
|
|
candidate: ConnectionCandidate,
|
|
) -> Result<Self> {
|
|
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?
|
|
}
|
|
};
|
|
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(addr: SocketAddr, hostname: &str, strict_tls: bool) -> Result<Self> {
|
|
let tls_stream = connect_tls_inner(addr, hostname, strict_tls, alpn(addr.port())).await?;
|
|
let buffered_stream = BufWriter::new(tls_stream);
|
|
let session_stream: Box<dyn SessionStream> = 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(addr: SocketAddr) -> Result<Self> {
|
|
let tcp_stream = connect_tcp_inner(addr).await?;
|
|
let buffered_stream = BufWriter::new(tcp_stream);
|
|
let session_stream: Box<dyn SessionStream> = 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(addr: SocketAddr, host: &str, strict_tls: bool) -> Result<Self> {
|
|
let tcp_stream = connect_tcp_inner(addr).await?;
|
|
|
|
// 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, &[], tcp_stream)
|
|
.await
|
|
.context("STARTTLS upgrade failed")?;
|
|
|
|
let buffered_stream = BufWriter::new(tls_stream);
|
|
let session_stream: Box<dyn SessionStream> = 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<Self> {
|
|
let proxy_stream = proxy_config
|
|
.connect(context, domain, port, strict_tls)
|
|
.await?;
|
|
let tls_stream = wrap_tls(strict_tls, domain, alpn(port), proxy_stream).await?;
|
|
let buffered_stream = BufWriter::new(tls_stream);
|
|
let session_stream: Box<dyn SessionStream> = 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<Self> {
|
|
let proxy_stream = proxy_config.connect(context, domain, port, false).await?;
|
|
let buffered_stream = BufWriter::new(proxy_stream);
|
|
let session_stream: Box<dyn SessionStream> = 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<Self> {
|
|
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, &[], proxy_stream)
|
|
.await
|
|
.context("STARTTLS upgrade failed")?;
|
|
let buffered_stream = BufWriter::new(tls_stream);
|
|
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
|
|
let client = Client::new(session_stream);
|
|
Ok(client)
|
|
}
|
|
}
|