Compare commits

...

3 Commits

Author SHA1 Message Date
Simon Laux
bb87e1fba4 add missing initialization of the new context property 2026-01-19 20:04:57 +01:00
Simon Laux
420b36fcde introduce shortlived in-memory dns cache and prefill ip addresses into
it during configuration.
2026-01-19 19:55:26 +01:00
Simon Laux
252fc8480e add dns_prefill attribute to entered login params and implement parsing
for it
2026-01-19 19:55:23 +01:00
8 changed files with 100 additions and 1 deletions

View File

@@ -54,6 +54,9 @@ pub struct EnteredLoginParam {
/// If true, login via OAUTH2 (not recommended anymore).
/// Default: false
pub oauth2: Option<bool>,
/// IP addresses for prefilling DNS
pub dns_prefill: Vec<String>,
}
impl From<dc::EnteredLoginParam> for EnteredLoginParam {
@@ -75,6 +78,7 @@ impl From<dc::EnteredLoginParam> 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<EnteredLoginParam> for dc::EnteredLoginParam {
},
certificate_checks: param.certificate_checks.unwrap_or_default().into(),
oauth2: param.oauth2.unwrap_or_default(),
dns_prefill: param.dns_prefill,
})
}
}

View File

@@ -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<Option<&'
let ctx2 = ctx.clone();
let update_device_chats_handle = task::spawn(async move { ctx2.update_device_chats().await });
if !param.dns_prefill.is_empty() {
let mut ips: Vec<IpAddr> = Vec::new();
for ip in &param.dns_prefill {
match ip.parse::<IpAddr>() {
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());

View File

@@ -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<RwLock<HashMap<String, Vec<std::net::IpAddr>>>>,
}
/// 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 {

View File

@@ -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<String>,
}
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!(

View File

@@ -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 {

View File

@@ -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<Qr> {
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<String> = match Url::parse(qr) {
Ok(url) => {
let options = url.query_pairs();
let parameter_map: BTreeMap<String, String> = 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)

View File

@@ -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)
}

View File

@@ -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;