diff --git a/deltachat-jsonrpc/src/api.rs b/deltachat-jsonrpc/src/api.rs index 857224d92..564e7200f 100644 --- a/deltachat-jsonrpc/src/api.rs +++ b/deltachat-jsonrpc/src/api.rs @@ -525,6 +525,18 @@ impl CommandApi { ctx.add_transport_from_qr(&qr).await } + /// Automatically adds up to three transports. + /// + /// If the user just scanned a QR code of type `Account`, `Login`, + /// `AskVerifyContact`, `AskVerifyGroup`, or `AskJoinBroadcast`, + /// then UI implementations should pass it as the `qr` parameter. + /// The host(s) from the QR code will then also be considered + /// for creating an account there. + async fn init_transports(&self, account_id: u32, qr: Option) -> Result<()> { + let ctx = self.get_context(account_id).await?; + ctx.init_transports(qr.as_deref()).await + } + /// Returns the list of all email accounts that are used as a transport in the current profile. /// Use [Self::add_or_update_transport()] to add or change a transport /// and [Self::delete_transport()] to remove a transport. diff --git a/src/autorelay.rs b/src/autorelay.rs index 4037da380..e92b06096 100644 --- a/src/autorelay.rs +++ b/src/autorelay.rs @@ -6,19 +6,25 @@ //! which migrations seed with a list of known chatmail relays. //! //! Status of implementation: -//! Additions are attempted right before going into IMAP IDLE, -//! i.e. only while connected and with nothing more important to do, -//! and only if a UI opted in via [`Config::Autorelay`]. -//! Once a profile has reached `NUM_TRANSPORTS_TARGET` transports, -//! [`Config::AutorelayFinished`] is set and nothing is ever added again, -//! so deleting a transport later does not pull in a replacement. +//! +//! - When the UI uses `init_transports()`, the user gets 3 randomly selected relays. +//! +//! - Later additions are attempted right before going into IMAP IDLE, +//! i.e. only while connected and with nothing more important to do, +//! and only if a UI opted in via [`Config::Autorelay`]. +//! Once a profile has reached `NUM_TRANSPORTS_TARGET` transports, +//! [`Config::AutorelayFinished`] is set and nothing is ever added again, +//! so deleting a transport later does not pull in a replacement. +use std::collections::{BTreeMap, BTreeSet}; use std::pin::Pin; use anyhow::Result; -use deltachat_contact_tools::addr_normalize; +use deltachat_contact_tools::{EmailAddress, addr_normalize}; use rand::distr::{Alphanumeric, SampleString}; -use rand::seq::IndexedRandom; +use rand::rng; +use rand::seq::{IndexedRandom, SliceRandom as _}; +use tokio::task::JoinSet; use crate::config::{self, Config}; use crate::log::{LogExt, warn}; @@ -32,6 +38,90 @@ 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 +const DEFAULT_RELAY_CANDIDATES: &[&str] = &[ + "chat.adminforge.de", + "tarpit.fun", + "sweetfern.net", + "chat.nuvon.app", + "nchrcht.la10cy.net", + "chat.sus.fr", + "chat.tinydispatch.org", + "chtml.ca", + "chatmail.uk", +]; + +pub(crate) async fn init_transports_inner( + context: &Context, + addrs_from_qr: Vec, +) -> Result<(), anyhow::Error> { + // TODO The default relay candidates need to be updated in the database, too. + // It would be annoying to have to write a migration everytime a relay candidate comes or goes; + // The solution is to make the relay candidates list into a const, + // and check in `load_relay_candidates()` whether any of them should be added + // rather than in a migration + // (if we need to remove some later, we will then need another const `REMOVED_RELAY_CANDIDATES` which are ignored; + // or alternatively we could use `relay_candidates` table only for saving the last used timestamps, + // and if we later want to add other sources for relay candidates then we need another table for that) + let mut candidates: Vec<&str> = DEFAULT_RELAY_CANDIDATES.into(); + candidates.shuffle(&mut rng()); + + let (relays_sender, relays_receiver) = async_channel::unbounded::(); + for addr in addrs_from_qr { + let email = EmailAddress::new(&addr)?; + relays_sender.try_send(email.domain)?; + } + for relay in candidates { + relays_sender.try_send(relay.to_string())?; + } + + let mut join_set = JoinSet::new(); + for _ in 0..NUM_TRANSPORTS_TARGET { + let context = context.clone(); + let relays_receiver = relays_receiver.clone(); + join_set.spawn(async move { + loop { + // Take a lock in order to prevent other relay management code + // from running simultaneously + let _lock = context.background_task_lock.read(); + + let Ok(host) = relays_receiver.try_recv() else { + return false; // No more relays to try + }; + let param = login_param_from_host(&host); + let skip_network = false; + let res = crate::configure::configure(&context, ¶m, skip_network).await; + if let Err(err) = res { + warn!(context, "Failed to init transport {host}: {err:#}."); + // Try another relay in the next iteration of the loop + } else { + if context.count_transports().await.unwrap_or(0) >= NUM_TRANSPORTS_TARGET { + context + .set_config_bool(Config::AutorelayFinished, true) + .await + .log_err(&context) + .ok(); + } + return true; // Success + } + } + }); + } + + while let Some(success) = join_set.join_next().await { + if success? { + break; + } + // If this task was not successful, continue waiting for the other tasks + // because maybe one of the ongoing configuration attempts will be successful + } + + join_set.detach_all(); + + context.set_config_bool(Config::Autorelay, true).await?; + + Ok(()) +} + pub(crate) fn maybe_add_additional_relays( context: Context, ) -> Pin + Send>> { @@ -57,7 +147,7 @@ pub(crate) fn maybe_add_additional_relays( 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 { + let Ok(_lock) = context.background_task_lock.try_write() else { // Housekeeping or automatic relay management is already running in another thread, do nothing. return Ok(false); }; @@ -134,7 +224,7 @@ async fn maybe_add_additional_relays_inner(context: &Context, skip_network: bool Ok(relay_added) } -async fn load_relay_candidates(context: &Context, now: i64) -> Result, anyhow::Error> { +pub(crate) async fn load_relay_candidates(context: &Context, now: i64) -> Result> { let cutoff_timestamp = now.saturating_sub(BACKOFF_PERIOD_FOR_NOT_WORKING_RELAY); let candidates: Vec = context .sql diff --git a/src/autorelay/autorelay_tests.rs b/src/autorelay/autorelay_tests.rs index 9f249f7ad..21dde7980 100644 --- a/src/autorelay/autorelay_tests.rs +++ b/src/autorelay/autorelay_tests.rs @@ -97,7 +97,7 @@ async fn test_maybe_add_additional_relays_mutex_held() -> Result<()> { // Hold the housekeeping mutex ourselves, simulating another task // already running housekeeping or relay management. - let _lock = t.background_task_mutex.lock().await; + let _lock = t.background_task_lock.write().await; assert_autorelay_does_nothing(t).await; diff --git a/src/configure.rs b/src/configure.rs index 57670059b..50885d4ce 100644 --- a/src/configure.rs +++ b/src/configure.rs @@ -40,7 +40,7 @@ use crate::transport::{ ConnectionCandidate, delete_transport_row, maybe_update_sending_transport, purge_transport_caches, send_sync_transports, transport_addrs, }; -use crate::{EventType, stock_str}; +use crate::{EventType, autorelay, stock_str}; /// Maximum number of relays. /// @@ -190,6 +190,40 @@ impl Context { Ok(()) } + /// Automatically adds up to three transports. + /// + /// If the user just scanned a QR code of type `Account`, `Login`, + /// `AskVerifyContact`, `AskVerifyGroup`, or `AskJoinBroadcast`, + /// then UI implementations should pass it as the `qr` parameter. + /// The host(s) from the QR code will then also be considered + /// for creating an account there. + pub async fn init_transports(&self, qr: Option<&str>) -> Result<()> { + if self.is_configured().await? { + bail!("Transports are already initialized"); + } + self.stop_io().await; + + let mut addrs_from_qr = vec![]; + + if let Some(qr) = qr { + match crate::qr::check_qr(self, qr).await? { + crate::qr::Qr::Account { .. } | crate::qr::Qr::Login { .. } => { + return self.add_transport_from_qr(qr).await; + } + crate::qr::Qr::AskVerifyContact { addrs, .. } + | crate::qr::Qr::AskVerifyGroup { addrs, .. } + | crate::qr::Qr::AskJoinBroadcast { addrs, .. } => addrs_from_qr = addrs, + _ => bail!("This QR code can't be used to initialize transports"), + } + } + + autorelay::init_transports_inner(self, addrs_from_qr).await?; + + self.start_io().await; + + Ok(()) + } + /// Returns the list of all email accounts that are used as a transport in the current profile. /// Use [Self::add_or_update_transport()] to add or change a transport /// and [Self::delete_transport()] to delete a transport. diff --git a/src/context.rs b/src/context.rs index 5e5209a53..d062611b3 100644 --- a/src/context.rs +++ b/src/context.rs @@ -231,8 +231,8 @@ pub struct InnerContext { /// This is a global mutex-like state for operations which should be modal in the /// clients. running_state: RwLock, - /// Mutex to prevent running housekeeping or relay management from multiple threads at once. - pub(crate) background_task_mutex: Mutex<()>, + /// Lock to prevent running housekeeping or relay management from multiple threads at once. + pub(crate) background_task_lock: RwLock<()>, /// Mutex to prevent multiple IMAP loops from fetching the messages at once. /// @@ -487,7 +487,7 @@ impl Context { blobdir, running_state: RwLock::new(Default::default()), sql: Sql::new(dbfile), - background_task_mutex: Mutex::new(()), + background_task_lock: RwLock::new(()), fetch_msgs_mutex: Mutex::new(()), translated_stockstrings: stockstrings, events, diff --git a/src/sql.rs b/src/sql.rs index ff1e722e7..bc8030cf8 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -782,7 +782,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.background_task_mutex.try_lock() else { + let Ok(_housekeeping_lock) = context.background_task_lock.try_write() else { // Housekeeping is already running in another thread, do nothing. return Ok(()); };