diff --git a/deltachat-jsonrpc/src/api/types/login_param.rs b/deltachat-jsonrpc/src/api/types/login_param.rs index 6036709cdd..2bbc16f43c 100644 --- a/deltachat-jsonrpc/src/api/types/login_param.rs +++ b/deltachat-jsonrpc/src/api/types/login_param.rs @@ -54,6 +54,9 @@ pub struct EnteredLoginParam { /// If true, login via OAUTH2 (not recommended anymore). /// Default: false pub oauth2: Option, + + /// IP addresses for prefilling DNS + pub dns_prefill: Vec, } impl From for EnteredLoginParam { @@ -75,6 +78,7 @@ impl From for EnteredLoginParam { smtp_password: param.smtp.password.into_option(), certificate_checks: certificate_checks.into_option(), oauth2: param.oauth2.into_option(), + dns_prefill: param.dns_prefill, } } } @@ -101,6 +105,7 @@ impl TryFrom for dc::EnteredLoginParam { }, certificate_checks: param.certificate_checks.unwrap_or_default().into(), oauth2: param.oauth2.unwrap_or_default(), + dns_prefill: param.dns_prefill, }) } } diff --git a/src/configure.rs b/src/configure.rs index 59f624ed58..09a4d86e46 100644 --- a/src/configure.rs +++ b/src/configure.rs @@ -13,6 +13,8 @@ mod auto_mozilla; mod auto_outlook; pub(crate) mod server_params; +use std::net::IpAddr; + use anyhow::{Context as _, Result, bail, ensure, format_err}; use auto_mozilla::moz_autoconfigure; use auto_outlook::outlk_autodiscover; @@ -557,6 +559,25 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result = Vec::new(); + for ip in ¶m.dns_prefill { + match ip.parse::() { + Ok(ip) => ips.push(ip), + Err(err) => { + error!( + ctx, + "IP address prefill failed: parsing of address '{ip}' failed: {err}" + ); + } + } + } + ctx.dns_memory_cache + .write() + .await + .insert(param.imap.server.clone(), ips); + } + let configured_param = get_configured_param(ctx, param).await?; let proxy_config = ProxyConfig::load(ctx).await?; let strict_tls = configured_param.strict_tls(proxy_config.is_some()); diff --git a/src/context.rs b/src/context.rs index 5461dc8245..25a327c23f 100644 --- a/src/context.rs +++ b/src/context.rs @@ -318,6 +318,11 @@ pub struct InnerContext { ) -> mail_builder::mime::MimePart<'a>, >, >, + + /// Short lived DNS cache which only lives in memory. + /// Used for configuration from `dcaccount` links with ip address. + /// Like `dcaccount:example.org?a=127.0.0.1,[::1]` + pub(crate) dns_memory_cache: Arc>>>, } /// The state of ongoing process. @@ -494,6 +499,7 @@ impl Context { self_fingerprint: OnceLock::new(), connectivities: parking_lot::Mutex::new(Vec::new()), pre_encrypt_mime_hook: None.into(), + dns_memory_cache: Arc::new(RwLock::new(HashMap::new())), }; let ctx = Context { diff --git a/src/login_param.rs b/src/login_param.rs index 5c6864f118..1a5e74dc65 100644 --- a/src/login_param.rs +++ b/src/login_param.rs @@ -97,6 +97,9 @@ pub struct EnteredLoginParam { /// If true, login via OAUTH2 (not recommended anymore) pub oauth2: bool, + + /// IP addresses for prefilling DNS + pub dns_prefill: Vec, } impl EnteredLoginParam { @@ -191,6 +194,7 @@ impl EnteredLoginParam { }, certificate_checks, oauth2, + dns_prefill: Default::default(), }) } @@ -360,6 +364,7 @@ mod tests { }, certificate_checks: Default::default(), oauth2: false, + dns_prefill: Default::default(), }; param.save(&t).await?; assert_eq!( diff --git a/src/net/dns.rs b/src/net/dns.rs index ac6956446f..6165e5f19f 100644 --- a/src/net/dns.rs +++ b/src/net/dns.rs @@ -860,6 +860,14 @@ pub(crate) async fn lookup_host_with_cache( } } } + if let Some(ips) = context.dns_memory_cache.read().await.get(hostname) { + for ip in ips { + let addr = SocketAddr::new(*ip, port); + if !cache.contains(&addr) { + cache.push(addr); + } + } + } merge_with_cache(resolved_addrs, cache) } else { diff --git a/src/qr.rs b/src/qr.rs index a289d9aafa..0af1cdc298 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -12,6 +12,7 @@ use percent_encoding::{NON_ALPHANUMERIC, percent_decode_str, percent_encode}; use rand::TryRngCore as _; use rand::distr::{Alphanumeric, SampleString}; use serde::Deserialize; +use url::Url; use crate::config::Config; use crate::contact::{Contact, ContactId, Origin}; @@ -656,6 +657,7 @@ async fn decode_ideltachat(context: &Context, prefix: &str, qr: &str) -> Result< /// scheme: `DCACCOUNT:example.org` /// or `DCACCOUNT:https://example.org/new` /// or `DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3` +/// or `dcaccount:example.org?a=127.0.0.1,[::1]` fn decode_account(qr: &str) -> Result { let payload = qr .get(DCACCOUNT_SCHEME.len()..) @@ -784,9 +786,33 @@ pub(crate) async fn login_param_from_account_qr( if !payload.starts_with(HTTPS_SCHEME) { let rng = &mut rand::rngs::OsRng.unwrap_err(); let username = Alphanumeric.sample_string(rng, 9); - let addr = username + "@" + payload; + let host = if let Some(start_of_query) = payload.find("?") { + payload + .get(..start_of_query) + .context("failed to ignore query part")? + } else { + payload + }; + let addr = username + "@" + host; let password = Alphanumeric.sample_string(rng, 50); + let dns_prefill: Vec = match Url::parse(qr) { + Ok(url) => { + let options = url.query_pairs(); + let parameter_map: BTreeMap = options + .map(|(key, value)| (key.into_owned(), value.into_owned())) + .collect(); + parameter_map + .get("a") + .map(|ips| ips.split(",").map(|s| s.to_owned()).collect()) + .unwrap_or_default() + } + Err(err) => { + error!(context, "error parsing parameter of account url: {err}"); + Default::default() + } + }; + let param = EnteredLoginParam { addr, imap: EnteredServerLoginParam { @@ -796,6 +822,7 @@ pub(crate) async fn login_param_from_account_qr( smtp: Default::default(), certificate_checks: EnteredCertificateChecks::Strict, oauth2: false, + dns_prefill, }; return Ok(param); } @@ -816,6 +843,7 @@ pub(crate) async fn login_param_from_account_qr( smtp: Default::default(), certificate_checks: EnteredCertificateChecks::Strict, oauth2: false, + dns_prefill: Default::default(), }; Ok(param) diff --git a/src/qr/dclogin_scheme.rs b/src/qr/dclogin_scheme.rs index cd43524985..5262f5d611 100644 --- a/src/qr/dclogin_scheme.rs +++ b/src/qr/dclogin_scheme.rs @@ -191,6 +191,7 @@ pub(crate) fn login_param_from_login_qr( }, certificate_checks: certificate_checks.unwrap_or_default(), oauth2: false, + dns_prefill: Default::default(), }; Ok(param) } diff --git a/src/qr/qr_tests.rs b/src/qr/qr_tests.rs index d6dc88f726..d5e00357e3 100644 --- a/src/qr/qr_tests.rs +++ b/src/qr/qr_tests.rs @@ -711,6 +711,31 @@ async fn test_decode_account() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_decode_account_with_dns_prefill() -> Result<()> { + let ctx = &TestContext::new().await; + + for (qr, prefill_ips) in [ + ( + "dcaccount:example.org?a=127.0.0.1,[::1]", + vec!["127.0.0.1", "[::1]"], + ), + ( + "DCACCOUNT:example.org?a=127.0.0.1,[::1]", + vec!["127.0.0.1", "[::1]"], + ), + ("dcaccount:example.org?a=[::1]", vec!["[::1]"]), + ("DCACCOUNT:example.org?a=127.0.0.1", vec!["127.0.0.1"]), + ] { + let param = login_param_from_account_qr(ctx, qr).await?; + println!("addr {}", param.addr); + assert!(param.addr.ends_with("example.org")); + assert_eq!(param.dns_prefill, prefill_ips); + } + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_decode_tg_socks_proxy() -> Result<()> { let t = TestContext::new().await;