Add init_transports() API

This commit is contained in:
Hocuri
2026-09-16 23:11:30 +02:00
parent 44c2febbe1
commit d5d8876b62
6 changed files with 152 additions and 16 deletions

View File

@@ -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<String>) -> 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.

View File

@@ -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<String>,
) -> 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::<String>();
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, &param, 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<Box<dyn Future<Output = ()> + 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<bool> {
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<Vec<String>, anyhow::Error> {
pub(crate) async fn load_relay_candidates(context: &Context, now: i64) -> Result<Vec<String>> {
let cutoff_timestamp = now.saturating_sub(BACKOFF_PERIOD_FOR_NOT_WORKING_RELAY);
let candidates: Vec<String> = context
.sql

View File

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

View File

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

View File

@@ -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<RunningState>,
/// 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,

View File

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