From db13d08f6cb7d4afe20eaf66781eb05b7b34a43d Mon Sep 17 00:00:00 2001 From: Hocuri Date: Fri, 31 Jul 2026 13:14:43 +0200 Subject: [PATCH] feat: Basic multi-relay onboarding (#8444) If multi-relay onboarding config is set from UIs, automatically add relays until there are 3 relays. For now, there will be a hardcoded list of relay candidates. - We need a list of chatmail relays that we somehow trust, and that agree to be in the list. Then, we add all of them to the candidate list (a new SQL table with colums "host" and "last_tried"). - Before going to IMAP IDLE: When there are less than 3 relays, fill it up with relays from the candidate list. If creating an account fails, try again with another relay from the list. For each candidate, we need to remember the last time we tried to add a transport there, and try at most once a week or so per candidate. - For now, this will be behind an off-by-default config option, which at least DC Android will enable when creating a new profile. UIs can then opt in on their own pace but will likely need to disable it for tests. - Right now, the backoff times are: Try to add a relay at most once per hour, and try to add the same relay at most once per week --- src/automatic_relay_management.rs | 178 +++++++++++ .../automatic_relay_management_tests.rs | 286 ++++++++++++++++++ src/config.rs | 9 + src/configure.rs | 137 ++++----- src/context.rs | 24 +- src/imap/idle.rs | 6 + src/lib.rs | 1 + src/qr.rs | 19 +- src/sql.rs | 2 +- src/sql/migrations.rs | 33 ++ 10 files changed, 604 insertions(+), 91 deletions(-) create mode 100644 src/automatic_relay_management.rs create mode 100644 src/automatic_relay_management/automatic_relay_management_tests.rs diff --git a/src/automatic_relay_management.rs b/src/automatic_relay_management.rs new file mode 100644 index 000000000..542a4da77 --- /dev/null +++ b/src/automatic_relay_management.rs @@ -0,0 +1,178 @@ +use std::pin::Pin; + +use anyhow::Result; +use deltachat_contact_tools::addr_normalize; +use rand::distr::{Alphanumeric, SampleString}; +use rand::seq::IndexedRandom; + +use crate::config::{self, Config}; +use crate::log::{LogExt, warn}; +use crate::login_param::{EnteredCertificateChecks, EnteredImapLoginParam}; +use crate::{configure::EnteredLoginParam, context::Context, tools::time}; + +/// The target number of transports. +const NUM_TRANSPORTS_TARGET: usize = 3; +/// How often we want to try adding new relays. +const AUTOMATIC_ADDITION_DEBOUNCE_SECONDS: i64 = 60 * 60; // one hour +/// How long we ignore a relay candidate after failing to connect to it: +const BACKOFF_PERIOD_FOR_NOT_WORKING_RELAY: i64 = 60 * 60 * 24 * 7; // one week + +pub(crate) fn maybe_add_additional_relays( + context: Context, +) -> Pin + Send>> { + // We need to Box::pin the future because it wouldn't compile otherwise + // because Rust async doesn't support recursion: + // `maybe_add_additional_relays_inner()` calls `restart_io_if_running()`, + // which (via several other functions) calls `imap_loop()`, + // which (via several other functions) calls `maybe_add_additional_relays()` + Box::pin(async move { + let skip_network = false; + let relay_added = maybe_add_additional_relays_inner(&context, skip_network) + .await + .log_err(&context) + .unwrap_or(false); + + if relay_added { + info!(context, "Restarting IO after relay addition"); + context.restart_io_if_running().await; + } + }) +} + +async fn maybe_add_additional_relays_inner(context: &Context, skip_network: bool) -> Result { + let now = time(); + + let Ok(_lock) = context.background_task_mutex.try_lock() else { + // Housekeeping or automatic relay management is already running in another thread, do nothing. + return Ok(false); + }; + let last_timestamp = context + .get_config_i64(Config::LastAutomaticRelayManagement) + .await?; + if last_timestamp > now { + warn!( + context, + "Clock ran backwards, unclear if automatic relay management should run. Will run it anyways." + ); + } else if last_timestamp > now.saturating_sub(AUTOMATIC_ADDITION_DEBOUNCE_SECONDS) { + return Ok(false); + } + if !context + .get_config_bool(Config::AutomaticRelayManagement) + .await? + { + return Ok(false); + } + if context + .get_config_bool(Config::AutomaticRelayManagementFinished) + .await? + { + return Ok(false); + } + // Set the config at the beginning to avoid endless loops. + // Race conditions are not a concern because we locked the mutex. + context + .set_config_internal(Config::LastAutomaticRelayManagement, Some(&now.to_string())) + .await?; + + let mut relay_added = false; + // Using `for` instead of `while` to prevent infinite loop + for _ in 0..NUM_TRANSPORTS_TARGET { + if context.count_transports().await? >= NUM_TRANSPORTS_TARGET { + context + .set_config_internal( + Config::AutomaticRelayManagementFinished, + config::from_bool(true), + ) + .await?; + + return Ok(relay_added); + } + + // First, query all candidates that were not tried since `BACKOFF_PERIOD_FOR_NOT_WORKING_RELAY` seconds. + // Hosts that are already used are excluded. + let candidates = load_relay_candidates(context, now).await?; + + let Some(host) = candidates.choose(&mut rand::rng()) else { + info!( + context, + "maybe_add_additional_relays: No suitable candidates" + ); + return Ok(relay_added); + }; + + info!( + context, + "Trying to automatically add relay {host} (there were {} candidates).", + candidates.len(), + ); + + context + .sql + .execute( + "UPDATE relay_candidates SET last_tried=? WHERE host=?", + (now, host), + ) + .await?; + let param = login_param_from_host(host); + let res = crate::configure::configure(context, ¶m, skip_network).await; + if let Err(e) = res { + warn!( + context, + "Failed to automatically add a relay {host}: {e:#}." + ); + } else { + info!(context, "Successfully automatically added relay {host}."); + relay_added = true; + } + } + + Ok(relay_added) +} + +async fn load_relay_candidates(context: &Context, now: i64) -> Result, anyhow::Error> { + let cutoff_timestamp = now.saturating_sub(BACKOFF_PERIOD_FOR_NOT_WORKING_RELAY); + let candidates: Vec = context + .sql + .query_map_vec( + // This also selects candidates which have last_tried in the future, + // essentially treating them as never tried, + // so if some timestamp far in the future is accidentally stored, + // we are not stuck never trying the candidate. + // After trying the candidate, last_tried will be corrected to the current time. + "SELECT host FROM relay_candidates WHERE (last_tried?) + AND NOT EXISTS ( + SELECT 1 + FROM transports + WHERE substr(addr, instr(addr, '@') + 1) = host + )", + (cutoff_timestamp, now), + |row| Ok(row.get::<_, String>(0)?), + ) + .await?; + + Ok(candidates) +} + +pub(crate) fn login_param_from_host(host: &str) -> EnteredLoginParam { + let rng = &mut rand::rng(); + let username = Alphanumeric.sample_string(rng, 9); + let addr = username + "@" + host; + let addr = addr_normalize(&addr); + // 22 * log2(26 * 2 + 10) = 130 bits of entropy + let password = Alphanumeric.sample_string(rng, 22); + + EnteredLoginParam { + addr, + imap: EnteredImapLoginParam { + password, + ..Default::default() + }, + smtp: Default::default(), + certificate_checks: EnteredCertificateChecks::Strict, + oauth2: false, + } +} + +#[cfg(test)] +mod automatic_relay_management_tests; diff --git a/src/automatic_relay_management/automatic_relay_management_tests.rs b/src/automatic_relay_management/automatic_relay_management_tests.rs new file mode 100644 index 000000000..e92b8d564 --- /dev/null +++ b/src/automatic_relay_management/automatic_relay_management_tests.rs @@ -0,0 +1,286 @@ +use std::time::Duration; + +use super::*; +use crate::test_utils::TestContext; +use crate::tools::SystemTime; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_load_relay_candidates_single() -> Result<()> { + let t = &TestContext::new_alice().await; + enable_config(t).await; + let now = time(); + + t.sql.execute("DELETE FROM relay_candidates", ()).await?; + + // This host should be returned by load_relay_candidates(): + t.sql + .execute( + "INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)", + ("never_tried.example", 0), + ) + .await?; + + // This host was recently tried and should not be returned: + t.sql + .execute( + "INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)", + ("recent.example", now), + ) + .await?; + + // This host is already in use (alice@example.org) and should not be returned: + t.sql + .execute( + "INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)", + ("example.org", 0), + ) + .await?; + + let candidates = load_relay_candidates(t, now).await?; + + assert_eq!(candidates, vec!["never_tried.example".to_string()]); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_load_relay_candidates_multiple() -> Result<()> { + let t = &TestContext::new().await; + enable_config(t).await; + let now = time(); + + t.sql.execute("DELETE FROM relay_candidates", ()).await?; + for host in ["a.example", "b.example", "c.example"] { + t.sql + .execute( + "INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)", + (host, 0), + ) + .await?; + } + + let mut candidates = load_relay_candidates(t, now).await?; + candidates.sort(); + + assert_eq!( + candidates, + vec![ + "a.example".to_string(), + "b.example".to_string(), + "c.example".to_string() + ] + ); + Ok(()) +} + +async fn assert_automatic_relay_management_does_nothing(t: &TestContext) { + let transports_before = t.count_transports().await.unwrap(); + let config_before = t + .get_config_i64(Config::LastAutomaticRelayManagement) + .await + .unwrap(); + + let skip_network = false; // No need to skip network, nothing is supposed to happen + let relay_added = maybe_add_additional_relays_inner(t, skip_network) + .await + .unwrap(); + assert_eq!(relay_added, false); + + let config_after = t + .get_config_i64(Config::LastAutomaticRelayManagement) + .await + .unwrap(); + let transports_after = t.count_transports().await.unwrap(); + + assert_eq!(config_after, config_before); + assert_eq!(transports_before, transports_after); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_maybe_add_additional_relays_mutex_held() -> Result<()> { + let t = &TestContext::new().await; + enable_config(t).await; + + // Hold the housekeeping mutex ourselves, simulating another task + // already running housekeeping or relay management. + let _lock = t.background_task_mutex.lock().await; + + assert_automatic_relay_management_does_nothing(t).await; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_maybe_add_additional_relays_debounce() -> Result<()> { + let t = &TestContext::new_alice().await; + enable_config(t).await; + let some_seconds_ago = time() - 10; + + // Pretend automatic relay management just ran. + t.set_config_internal( + Config::LastAutomaticRelayManagement, + Some(&some_seconds_ago.to_string()), + ) + .await?; + + assert_automatic_relay_management_does_nothing(t).await; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_maybe_add_additional_relays_disabled() { + // By default, automatic relay management is disabled: + let t = &TestContext::new_alice().await; + assert_automatic_relay_management_does_nothing(t).await; +} + +/// Runs maybe_add_additional_relays_inner(), then deletes one of the transports. +/// Even after AUTOMATIC_ADDITION_DEBOUNCE_SECONDS, +/// running automatic transport management again should not add back a transport. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_maybe_add_additional_relays_does_nothing_after_finishing_once() -> Result<()> { + let t = &TestContext::new_alice().await; + enable_config(t).await; + + let skip_network = true; + let relay_added = maybe_add_additional_relays_inner(t, skip_network).await?; + assert!(relay_added); + + let transports = t.list_transports().await?; + t.delete_transport(&transports.last().unwrap().param.addr) + .await?; + + SystemTime::shift(Duration::from_secs( + AUTOMATIC_ADDITION_DEBOUNCE_SECONDS as u64 + 1, + )); + + let transports_count = t.count_transports().await?; + assert_eq!(transports_count, NUM_TRANSPORTS_TARGET - 1); + + assert!( + t.get_config_bool(Config::AutomaticRelayManagementFinished) + .await? + ); + assert_automatic_relay_management_does_nothing(t).await; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_maybe_add_additional_relays_add_one() -> Result<()> { + let t = &TestContext::new_alice().await; + enable_config(t).await; + let now = time(); + + t.sql.execute("DELETE FROM relay_candidates", ()).await?; + t.sql + .execute( + "INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)", + ("relay.example", 0), + ) + .await?; + + let transports_before = t.count_transports().await?; + + let skip_network = true; + let relay_added = maybe_add_additional_relays_inner(t, skip_network).await?; + assert!(relay_added); + + let config_after = t + .get_config_i64(Config::LastAutomaticRelayManagement) + .await?; + assert!(config_after >= now); + + let transports_after = t.count_transports().await?; + assert_eq!(transports_after, transports_before + 1); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_maybe_add_additional_relays_add_multiple() -> Result<()> { + let t = &TestContext::new_alice().await; + enable_config(t).await; + let now = time(); + + t.sql.execute("DELETE FROM relay_candidates", ()).await?; + for host in ["a.example", "b.example", "c.example", "d.example"] { + t.sql + .execute( + "INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)", + (host, 0), + ) + .await?; + } + + let skip_network = true; + let relay_added = maybe_add_additional_relays_inner(t, skip_network).await?; + assert!(relay_added); + + let config_after = t + .get_config_i64(Config::LastAutomaticRelayManagement) + .await?; + assert!(config_after >= now); + + let transports_after = t.count_transports().await?; + assert_eq!(transports_after, NUM_TRANSPORTS_TARGET); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_maybe_add_additional_relays_failure() -> Result<()> { + let t = &TestContext::new_alice().await; + enable_config(t).await; + let now = time(); + + t.sql.execute("DELETE FROM relay_candidates", ()).await?; + for i in 1..10 { + t.sql + .execute( + "INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)", + (format!("{i}.invalid.example"), 0), + ) + .await?; + } + + let transports_before = t.count_transports().await?; + + // Don't skip network, since we want the relay addition to fail + let skip_network = false; + let relay_added = maybe_add_additional_relays_inner(t, skip_network).await?; + assert_eq!(relay_added, false); + + // The config is still updated: + let config_after = t + .get_config_i64(Config::LastAutomaticRelayManagement) + .await?; + assert!(config_after >= now); + + let transports_after = t.count_transports().await?; + assert_eq!(transports_after, transports_before); + + // Some of the candidates should have an updated last_tried: + assert!( + t.sql + .exists( + "SELECT COUNT(*) FROM relay_candidates WHERE last_tried>=?", + (now,) + ) + .await? + ); + + // ...but not all, because there might be many relay candidates + // and we don't want to try all of them in a single call: + assert_eq!(load_relay_candidates(t, now).await?.is_empty(), false); + + Ok(()) +} + +async fn enable_config(context: &Context) { + context + .set_config_bool(Config::AutomaticRelayManagement, true) + .await + .unwrap(); +} diff --git a/src/config.rs b/src/config.rs index ed4a98739..df0a7b74c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -349,6 +349,15 @@ pub enum Config { /// Timestamp of the last `CantDecryptOutgoingMsgs` notification. LastCantDecryptOutgoingMsgs, + /// Timestamp of the last time automatic relay management was run + LastAutomaticRelayManagement, + + /// Whether to automatically add/remove transports + AutomaticRelayManagement, + + /// Whether automatic relay management successfully added the desired number of relays + AutomaticRelayManagementFinished, + /// Whether to avoid using IMAP IDLE even if the server supports it. /// /// This is a developer option for testing "fake idle". diff --git a/src/configure.rs b/src/configure.rs index fc45463fb..cc8139cb3 100644 --- a/src/configure.rs +++ b/src/configure.rs @@ -45,21 +45,6 @@ use crate::{EventType, stock_str}; /// See . pub(crate) const MAX_RELAYS: usize = 5; -/// Hard-coded candidates for default relays. -/// In the future, we want to use it during onboarding; -/// note that before onboarding automatically on any of these, -/// we need to ask the admins whether their relay is able to handle this. -/// For now, this is just the first 6 relays from chatmail.at/relays. -#[allow(unused)] -const DEFAULT_RELAY_CANDIDATES: &[&str] = &[ - "mehl.cloud", - "mailchat.pl", - "chatmail.woodpeckersnest.space", - "chatmail.culturanerd.it", - "tarpit.fun", - "d.gaufr.es", -]; - macro_rules! progress { ($context:tt, $progress:expr, $comment:expr) => { assert!( @@ -335,9 +320,10 @@ impl Context { self.try_make_space_for_new_relay().await?; } - if let Err(error) = configure(self, param).await { + let skip_network = false; + if let Err(error) = configure(self, param, skip_network).await { // Log entered and actual params - let configured_param = get_configured_param(self, param).await; + let configured_param = get_configured_param(self, param, skip_network).await; warn!( self, "configure failed: Entered params: {}. Used params: {}. Error: {error}.", @@ -401,6 +387,7 @@ impl Context { async fn get_configured_param( ctx: &Context, param: &EnteredLoginParam, + skip_network: bool, ) -> Result { ensure!(!param.addr.is_empty(), "Missing email address."); @@ -428,6 +415,7 @@ async fn get_configured_param( && param.smtp.port == 0 && param.smtp.security == Socket::Automatic && param.smtp.user.is_empty() + && !skip_network { // No advanced parameters entered by the user: // do Autoconfig unless the domain has hard-coded legacy servers. @@ -528,74 +516,82 @@ async fn get_configured_param( Ok(configured_login_param) } -async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<()> { +pub(crate) async fn configure( + ctx: &Context, + param: &EnteredLoginParam, + skip_network: bool, +) -> Result<()> { progress!(ctx, 1); - let configured_param = get_configured_param(ctx, param).await?; + let configured_param = get_configured_param(ctx, param, skip_network).await?; let proxy_config = ProxyConfig::load(ctx).await?; let strict_tls = configured_param.strict_tls(proxy_config.is_some())?; progress!(ctx, 550); - // Spawn SMTP configuration task - // to try SMTP while connecting to IMAP. - let context_smtp = ctx.clone(); - let smtp_param = configured_param.smtp.clone(); - let smtp_password = configured_param.smtp_password.clone(); - let smtp_addr = configured_param.addr.clone(); + if !skip_network { + // Spawn SMTP configuration task + // to try SMTP while connecting to IMAP. + let context_smtp = ctx.clone(); + let smtp_param = configured_param.smtp.clone(); + let smtp_password = configured_param.smtp_password.clone(); + let smtp_addr = configured_param.addr.clone(); - let proxy_config2 = proxy_config.clone(); - let smtp_config_task = task::spawn(async move { - let mut smtp = Smtp::new(); - smtp.connect( - &context_smtp, - &smtp_param, - &smtp_password, - &proxy_config2, - &smtp_addr, - strict_tls, - ) - .await?; + let proxy_config2 = proxy_config.clone(); + let smtp_config_task = task::spawn(async move { + let mut smtp = Smtp::new(); + smtp.connect( + &context_smtp, + &smtp_param, + &smtp_password, + &proxy_config2, + &smtp_addr, + strict_tls, + ) + .await?; - Ok::<(), anyhow::Error>(()) - }); + Ok::<(), anyhow::Error>(()) + }); - progress!(ctx, 600); + progress!(ctx, 600); - // Configure IMAP + // Configure IMAP - let transport_id = 0; - let (_s, r) = async_channel::bounded(1); - let mut imap = Imap::new(ctx, transport_id, configured_param.clone(), r).await?; - let configuring = true; - let imap_session = match imap.connect(ctx, configuring).await { - Ok(imap_session) => imap_session, - Err(err) => { - bail!("{}", nicer_configuration_error(ctx, format!("{err:#}"))); + let transport_id = 0; + let (_s, r) = async_channel::bounded(1); + let mut imap = Imap::new(ctx, transport_id, configured_param.clone(), r).await?; + let configuring = true; + let imap_session = match imap.connect(ctx, configuring).await { + Ok(imap_session) => imap_session, + Err(err) => { + bail!("{}", nicer_configuration_error(ctx, format!("{err:#}"))); + } + }; + + progress!(ctx, 850); + + // Wait for SMTP configuration + smtp_config_task.await??; + + progress!(ctx, 900); + + let is_configured = ctx.is_configured().await?; + if !ctx.get_config_bool(Config::FixIsChatmail).await? { + if imap_session.is_chatmail() { + ctx.sql.set_raw_config("is_chatmail", Some("1")).await?; + } else if !is_configured { + // Reset the setting that may have been set + // during failed configuration. + ctx.sql.set_raw_config("is_chatmail", Some("0")).await?; + } } - }; - progress!(ctx, 850); - - // Wait for SMTP configuration - smtp_config_task.await??; - - progress!(ctx, 900); - - let is_configured = ctx.is_configured().await?; - if !ctx.get_config_bool(Config::FixIsChatmail).await? { - if imap_session.is_chatmail() { - ctx.sql.set_raw_config("is_chatmail", Some("1")).await?; - } else if !is_configured { - // Reset the setting that may have been set - // during failed configuration. - ctx.sql.set_raw_config("is_chatmail", Some("0")).await?; - } + // Drop the imap connection explicitly + // to make sure that it's not forgotten in a future refactoring + drop(imap_session); + drop(imap); } - drop(imap_session); - drop(imap); - progress!(ctx, 910); configured_param @@ -779,7 +775,8 @@ mod tests { ..Default::default() }; - let configured_param = get_configured_param(t, &entered_param).await?; + let skip_network = false; + let configured_param = get_configured_param(t, &entered_param, skip_network).await?; assert_eq!(configured_param.imap_user, "alice@example.net"); assert_eq!(configured_param.smtp_user, ""); Ok(()) diff --git a/src/context.rs b/src/context.rs index 14031055c..e5782bd38 100644 --- a/src/context.rs +++ b/src/context.rs @@ -232,8 +232,8 @@ pub struct InnerContext { running_state: RwLock, /// Mutex to prevent a race condition when a "your pw is wrong" warning is sent, resulting in multiple messages being sent. pub(crate) wrong_pw_warning_mutex: Mutex<()>, - /// Mutex to prevent running housekeeping from multiple threads at once. - pub(crate) housekeeping_mutex: Mutex<()>, + /// Mutex to prevent running housekeeping or relay management from multiple threads at once. + pub(crate) background_task_mutex: Mutex<()>, /// Mutex to prevent multiple IMAP loops from fetching the messages at once. /// @@ -478,7 +478,7 @@ impl Context { running_state: RwLock::new(Default::default()), sql: Sql::new(dbfile), wrong_pw_warning_mutex: Mutex::new(()), - housekeeping_mutex: Mutex::new(()), + background_task_mutex: Mutex::new(()), fetch_msgs_mutex: Mutex::new(()), translated_stockstrings: stockstrings, events, @@ -1033,6 +1033,24 @@ impl Context { .await? .to_string(), ); + res.insert( + "last_automatic_relay_management", + self.get_config_i64(Config::LastAutomaticRelayManagement) + .await? + .to_string(), + ); + res.insert( + "automatic_relay_management", + self.get_config_bool(Config::AutomaticRelayManagement) + .await? + .to_string(), + ); + res.insert( + "automatic_relay_management_finished", + self.get_config_bool(Config::AutomaticRelayManagementFinished) + .await? + .to_string(), + ); let elapsed = time_elapsed(&self.creation_time); res.insert("uptime", duration_to_str(elapsed)); diff --git a/src/imap/idle.rs b/src/imap/idle.rs index 2f55684ef..4bf9b3f66 100644 --- a/src/imap/idle.rs +++ b/src/imap/idle.rs @@ -51,6 +51,12 @@ impl Session { return Ok(self); } + // we try to add additional relays right before going into IDLE mode, + // because we are connected and don't have anything important to do. + tokio::task::spawn( + crate::automatic_relay_management::maybe_add_additional_relays(context.clone()), + ); + let mut handle = self.inner.idle(); handle .init() diff --git a/src/lib.rs b/src/lib.rs index 4f5000dfa..92b84e87a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,6 +54,7 @@ pub(crate) mod events; pub use events::*; mod aheader; +mod automatic_relay_management; pub mod blob; pub mod calls; pub mod chat; diff --git a/src/qr.rs b/src/qr.rs index 9a0724955..0119e8d36 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -9,10 +9,9 @@ pub use dclogin_scheme::LoginOptions; pub(crate) use dclogin_scheme::login_param_from_login_qr; use deltachat_contact_tools::{ContactAddress, addr_normalize, may_be_valid_addr}; use percent_encoding::{NON_ALPHANUMERIC, percent_decode_str, percent_encode}; -use rand::TryRngCore as _; -use rand::distr::{Alphanumeric, SampleString}; use serde::Deserialize; +use crate::automatic_relay_management::login_param_from_host; use crate::config::Config; use crate::contact::{Contact, ContactId, Origin}; use crate::context::Context; @@ -828,21 +827,7 @@ pub(crate) async fn login_param_from_account_qr( .context("Invalid DCACCOUNT scheme")?; 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 password = Alphanumeric.sample_string(rng, 50); - - let param = EnteredLoginParam { - addr, - imap: EnteredImapLoginParam { - password, - ..Default::default() - }, - smtp: Default::default(), - certificate_checks: EnteredCertificateChecks::Strict, - oauth2: false, - }; + let param = login_param_from_host(payload); return Ok(param); } diff --git a/src/sql.rs b/src/sql.rs index 987aefb0a..729c16b55 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -780,7 +780,7 @@ async fn incremental_vacuum(context: &Context) -> Result<()> { /// Cleanup the account to restore some storage and optimize the database. pub async fn housekeeping(context: &Context) -> Result<()> { - let Ok(_housekeeping_lock) = context.housekeeping_mutex.try_lock() else { + let Ok(_housekeeping_lock) = context.background_task_mutex.try_lock() else { // Housekeeping is already running in another thread, do nothing. return Ok(()); }; diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index f435cc2db..ecaf59091 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -2534,6 +2534,39 @@ UPDATE msgs SET state=24 WHERE state=18; -- Change OutPreparing to OutFailed. .await?; } + inc_and_check(&mut migration_version, 161)?; + if dbversion < migration_version { + // TODO put a better list here + const DEFAULT_RELAY_CANDIDATES: &[&str] = &[ + "mehl.cloud", + "mailchat.pl", + "chatmail.woodpeckersnest.space", + "chatmail.culturanerd.it", + "tarpit.fun", + "d.gaufr.es", + ]; + + sql.execute_migration_transaction( + |transaction| { + transaction.execute( + "CREATE TABLE relay_candidates( + host TEXT PRIMARY KEY NOT NULL, + last_tried INTEGER NOT NULL DEFAULT 0 + ) STRICT", + (), + )?; + let mut statement = + transaction.prepare("INSERT INTO relay_candidates(host) VALUES (?)")?; + for host in DEFAULT_RELAY_CANDIDATES { + statement.execute((host,))?; + } + Ok(()) + }, + migration_version, + ) + .await?; + } + let new_version = sql .get_raw_config_int(VERSION_CFG) .await?