From 93200164d24ffe7280322f042b5e794e9e48dc2d Mon Sep 17 00:00:00 2001 From: Hocuri Date: Tue, 21 Jul 2026 14:23:56 +0200 Subject: [PATCH 1/8] [WIP] feat: Basic multi-relay onboarding --- src/sql/migrations.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index d1f901b5c6..a42349d264 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -2522,6 +2522,18 @@ UPDATE msgs SET state=24 WHERE state=18; -- Change OutPreparing to OutFailed. .await?; } + inc_and_check(&mut migration_version, 159)?; + if dbversion < migration_version { + sql.execute_migration( + "CREATE TABLE relay_candidates( + domain TEXT PRIMARY KEY NOT NULL, + last_tried TEXT NOT NULL DEFAULT 0 + ) STRICT", + migration_version, + ) + .await?; + } + let new_version = sql .get_raw_config_int(VERSION_CFG) .await? From 60bad51d207a88d7b440dd492fd7efa86007559d Mon Sep 17 00:00:00 2001 From: Hocuri Date: Tue, 21 Jul 2026 15:11:40 +0200 Subject: [PATCH 2/8] Add maybe_add_additional_transports() --- src/automatic_transport_management.rs | 93 +++++++++++++++++++++++++++ src/config.rs | 4 ++ src/imap/idle.rs | 9 ++- src/lib.rs | 1 + src/qr.rs | 16 +---- src/sql/migrations.rs | 1 + 6 files changed, 109 insertions(+), 15 deletions(-) create mode 100644 src/automatic_transport_management.rs diff --git a/src/automatic_transport_management.rs b/src/automatic_transport_management.rs new file mode 100644 index 0000000000..9e9f58845c --- /dev/null +++ b/src/automatic_transport_management.rs @@ -0,0 +1,93 @@ +use anyhow::Result; +use rand::TryRngCore as _; +use rand::distr::{Alphanumeric, SampleString}; +use rand::seq::IndexedRandom; + +use crate::config::Config; +use crate::login_param::{EnteredCertificateChecks, EnteredImapLoginParam}; +use crate::{configure::EnteredLoginParam, context::Context, tools::time}; + +/// The target number of transports we try to reach. +const NUM_TRANSPORTS_TARGET: usize = 3; +/// How often we want to try adding new transports. +const AUTOMATIC_ADDITION_DEBOUNCE_SECONDS: i64 = 60 * 60; // one hour +/// How long we ignore a transport candidate after failing to create an account there: +const BACKOFF_PERIOD_FOR_NOT_WORKING_TRANSPORT: i64 = 60 * 60 * 24 * 7; // one week + +// TODO think about how this interacts with stop_io()/start_io() +pub(crate) async fn maybe_add_additional_transports(context: &Context) -> Result<()> { + let now = time(); + + // TODO potentially rename housekeeping_mutex + let Ok(_housekeeping_lock) = context.housekeeping_mutex.try_lock() else { + // Housekeeping or automatic relay management is already running in another thread, do nothing. + return Ok(()); + }; + if context + .get_config_i64(Config::LastAutomaticTransportManagement) + .await? + > now.saturating_sub(AUTOMATIC_ADDITION_DEBOUNCE_SECONDS) + { + return Ok(()); + } + context + .set_config_internal( + Config::LastAutomaticTransportManagement, + Some(&now.to_string()), + ) + .await?; + + while context.count_transports().await? < NUM_TRANSPORTS_TARGET { + let cutoff_timestamp = now.saturating_sub(BACKOFF_PERIOD_FOR_NOT_WORKING_TRANSPORT); + + // TODO find out why login_param_from_account_qr() uses OsRng rather than rng() + let candidates: Vec = context + .sql + .query_map_vec( + "SELECT domain FROM relay_candidates WHERE last_tried(0)?), + ) + .await?; + + let Some(domain) = candidates.choose(&mut rand::rng()) else { + info!( + context, + "maybe_add_additional_relays: No suitable candidates" + ); + return Ok(()); + }; + + let mut param = login_param_from_domain(domain); + let res = context.add_transport_inner(&mut param).await; + if res.is_err() { + context + .sql + .execute("UPDATE relay_candidates SET last_tried=?", (now,)) + .await?; + } + res?; + } + + Ok(()) +} + +pub(crate) fn login_param_from_domain(domain: &str) -> EnteredLoginParam { + // TODO Question: BTW, why is this using OsRng rather than rng()? + // I just extracted this function, didn't change the code. + let rng = &mut rand::rngs::OsRng.unwrap_err(); + let username = Alphanumeric.sample_string(rng, 9); + let addr = username + "@" + domain; + let password = Alphanumeric.sample_string(rng, 50); + + let param = EnteredLoginParam { + addr, + imap: EnteredImapLoginParam { + password, + ..Default::default() + }, + smtp: Default::default(), + certificate_checks: EnteredCertificateChecks::Strict, + }; + param +} diff --git a/src/config.rs b/src/config.rs index 922c23b375..4edb74cc74 100644 --- a/src/config.rs +++ b/src/config.rs @@ -353,6 +353,10 @@ pub enum Config { /// Timestamp of the last `CantDecryptOutgoingMsgs` notification. LastCantDecryptOutgoingMsgs, + /// Timestamp of the last call to + /// [`crate::automatic_transport_management::maybe_add_additional_transports`] + LastAutomaticTransportManagement, + /// 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/imap/idle.rs b/src/imap/idle.rs index 2f55684ef9..dd3bb1eab0 100644 --- a/src/imap/idle.rs +++ b/src/imap/idle.rs @@ -8,7 +8,7 @@ use tokio::time::timeout; use super::Imap; use super::session::Session; use crate::context::Context; -use crate::log::warn; +use crate::log::{LogExt as _, warn}; use crate::net::TIMEOUT; use crate::tools::{self, time_elapsed}; @@ -51,6 +51,13 @@ impl Session { return Ok(self); } + // we try to add additional relays right before going into IDLE mode, + // because apparently we are connected, but don't have anything important to do. + crate::automatic_transport_management::maybe_add_additional_transports(context) + .await + .context("maybe_add_additional_relays") + .log_err(context); + let mut handle = self.inner.idle(); handle .init() diff --git a/src/lib.rs b/src/lib.rs index 233a4a2f64..9b03cbbab3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,6 +55,7 @@ pub(crate) mod events; pub use events::*; mod aheader; +mod automatic_transport_management; pub mod blob; pub mod calls; pub mod chat; diff --git a/src/qr.rs b/src/qr.rs index 4059382753..8c8a88a665 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -13,6 +13,7 @@ use rand::TryRngCore as _; use rand::distr::{Alphanumeric, SampleString}; use serde::Deserialize; +use crate::automatic_transport_management::login_param_from_domain; use crate::config::Config; use crate::contact::{Contact, ContactId, Origin}; use crate::context::Context; @@ -828,20 +829,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, - }; + let param = login_param_from_domain(payload); return Ok(param); } diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index a42349d264..822df299b3 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -2522,6 +2522,7 @@ UPDATE msgs SET state=24 WHERE state=18; -- Change OutPreparing to OutFailed. .await?; } + // TODO should we call it relay_candidates or transport_candidates? inc_and_check(&mut migration_version, 159)?; if dbversion < migration_version { sql.execute_migration( From dca929e9ddfab207ec5df94dbd97fbcfbf04bcfa Mon Sep 17 00:00:00 2001 From: Hocuri Date: Tue, 21 Jul 2026 16:36:48 +0200 Subject: [PATCH 3/8] Put it behind config option, fill `relay_candidates` with defaults, exclude candidates that are already in use --- src/automatic_transport_management.rs | 28 ++++++++++++++++++------ src/config.rs | 3 +++ src/configure.rs | 21 +++++------------- src/sql/migrations.rs | 31 ++++++++++++++++++++++----- 4 files changed, 56 insertions(+), 27 deletions(-) diff --git a/src/automatic_transport_management.rs b/src/automatic_transport_management.rs index 9e9f58845c..37e25d87d1 100644 --- a/src/automatic_transport_management.rs +++ b/src/automatic_transport_management.rs @@ -15,6 +15,9 @@ const AUTOMATIC_ADDITION_DEBOUNCE_SECONDS: i64 = 60 * 60; // one hour const BACKOFF_PERIOD_FOR_NOT_WORKING_TRANSPORT: i64 = 60 * 60 * 24 * 7; // one week // TODO think about how this interacts with stop_io()/start_io() + +// TODO decide if this should be done asynchronously in a task; +// generally we should be able to try adding relays while io is running. pub(crate) async fn maybe_add_additional_transports(context: &Context) -> Result<()> { let now = time(); @@ -37,14 +40,24 @@ pub(crate) async fn maybe_add_additional_transports(context: &Context) -> Result ) .await?; - while context.count_transports().await? < NUM_TRANSPORTS_TARGET { - let cutoff_timestamp = now.saturating_sub(BACKOFF_PERIOD_FOR_NOT_WORKING_TRANSPORT); + // Using `for` instead of `while` to prevent infinite loop + for _ in 0..NUM_TRANSPORTS_TARGET { + if context.count_transports().await? >= NUM_TRANSPORTS_TARGET { + return Ok(()); + } - // TODO find out why login_param_from_account_qr() uses OsRng rather than rng() + // First, query all candidates that were not tried since `BACKOFF_PERIOD_FOR_NOT_WORKING_TRANSPORT` seconds. + // Domains that are already used are excluded. + let cutoff_timestamp = now.saturating_sub(BACKOFF_PERIOD_FOR_NOT_WORKING_TRANSPORT); let candidates: Vec = context .sql .query_map_vec( - "SELECT domain FROM relay_candidates WHERE last_tried(0)?), ) @@ -58,14 +71,17 @@ pub(crate) async fn maybe_add_additional_transports(context: &Context) -> Result return Ok(()); }; - let mut param = login_param_from_domain(domain); - let res = context.add_transport_inner(&mut param).await; + let param = login_param_from_domain(domain); + let res = crate::configure::configure(context, ¶m).await; if res.is_err() { context .sql .execute("UPDATE relay_candidates SET last_tried=?", (now,)) .await?; } + + // TODO: Decide whether we want to immediately try again with another relay, + // if this one failed. If yes, remove the next line. res?; } diff --git a/src/config.rs b/src/config.rs index 4edb74cc74..c167733a8e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -357,6 +357,9 @@ pub enum Config { /// [`crate::automatic_transport_management::maybe_add_additional_transports`] LastAutomaticTransportManagement, + /// Whether to automatically add/remove transports + AutomaticTransportManagement, + /// 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 5c36a32dce..100547a7db 100644 --- a/src/configure.rs +++ b/src/configure.rs @@ -49,21 +49,6 @@ use crate::{chat, provider}; /// 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!( @@ -604,7 +589,11 @@ async fn get_configured_param( Ok(configured_login_param) } -async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result> { +// TODO maybe this function name should be changed +pub(crate) async fn configure( + ctx: &Context, + param: &EnteredLoginParam, +) -> Result> { progress!(ctx, 1); let configured_param = get_configured_param(ctx, param).await?; diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 822df299b3..3975425eb8 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -2525,11 +2525,32 @@ UPDATE msgs SET state=24 WHERE state=18; -- Change OutPreparing to OutFailed. // TODO should we call it relay_candidates or transport_candidates? inc_and_check(&mut migration_version, 159)?; if dbversion < migration_version { - sql.execute_migration( - "CREATE TABLE relay_candidates( - domain TEXT PRIMARY KEY NOT NULL, - last_tried TEXT NOT NULL DEFAULT 0 - ) STRICT", + // 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( + domain TEXT PRIMARY KEY NOT NULL, + last_tried TEXT NOT NULL DEFAULT 0 + ) STRICT", + (), + )?; + let mut statement = + transaction.prepare("INSERT INTO relay_candidates(domain) VALUES (?)")?; + for domain in DEFAULT_RELAY_CANDIDATES { + statement.execute((domain,)); + } + Ok(()) + }, migration_version, ) .await?; From df250572b89498ed0f53da4345169b16eedcc58b Mon Sep 17 00:00:00 2001 From: Hocuri Date: Wed, 22 Jul 2026 12:12:15 +0200 Subject: [PATCH 4/8] Use rng rather than OsRng --- src/automatic_transport_management.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/automatic_transport_management.rs b/src/automatic_transport_management.rs index 37e25d87d1..74d7d1be89 100644 --- a/src/automatic_transport_management.rs +++ b/src/automatic_transport_management.rs @@ -89,9 +89,7 @@ pub(crate) async fn maybe_add_additional_transports(context: &Context) -> Result } pub(crate) fn login_param_from_domain(domain: &str) -> EnteredLoginParam { - // TODO Question: BTW, why is this using OsRng rather than rng()? - // I just extracted this function, didn't change the code. - let rng = &mut rand::rngs::OsRng.unwrap_err(); + let rng = &mut rand::rng(); let username = Alphanumeric.sample_string(rng, 9); let addr = username + "@" + domain; let password = Alphanumeric.sample_string(rng, 50); From 2142509dd4353056c64936df30a4eacc3437da18 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Wed, 22 Jul 2026 14:42:05 +0200 Subject: [PATCH 5/8] --wip-- [skip ci] --- deltachat-jsonrpc/src/api.rs | 4 ++- src/automatic_transport_management.rs | 31 ++++++++++++++++- src/configure.rs | 48 +++++++++++++++------------ src/imap/idle.rs | 7 ++-- src/sql/migrations.rs | 2 +- src/transport.rs | 4 ++- 6 files changed, 67 insertions(+), 29 deletions(-) diff --git a/deltachat-jsonrpc/src/api.rs b/deltachat-jsonrpc/src/api.rs index 788a220f70..861df6b7d4 100644 --- a/deltachat-jsonrpc/src/api.rs +++ b/deltachat-jsonrpc/src/api.rs @@ -593,7 +593,9 @@ impl CommandApi { /// UI implementations must use [`Self::set_transport_unpublished`] instead. async fn delete_transport(&self, account_id: u32, addr: String) -> Result<()> { let ctx = self.get_context(account_id).await?; - ctx.delete_transport(&addr).await + ctx.delete_transport(&addr) + .await + .context("delete_transport") } /// Change whether the transport is unpublished. diff --git a/src/automatic_transport_management.rs b/src/automatic_transport_management.rs index 74d7d1be89..c15cc43e2d 100644 --- a/src/automatic_transport_management.rs +++ b/src/automatic_transport_management.rs @@ -1,10 +1,14 @@ +use std::pin::Pin; + use anyhow::Result; use rand::TryRngCore as _; use rand::distr::{Alphanumeric, SampleString}; use rand::seq::IndexedRandom; use crate::config::Config; +use crate::log::LogExt as _; use crate::login_param::{EnteredCertificateChecks, EnteredImapLoginParam}; +use crate::transport::restart_io_if_running_boxed; use crate::{configure::EnteredLoginParam, context::Context, tools::time}; /// The target number of transports we try to reach. @@ -18,12 +22,26 @@ const BACKOFF_PERIOD_FOR_NOT_WORKING_TRANSPORT: i64 = 60 * 60 * 24 * 7; // one w // TODO decide if this should be done asynchronously in a task; // generally we should be able to try adding relays while io is running. -pub(crate) async fn maybe_add_additional_transports(context: &Context) -> Result<()> { +pub(crate) async fn maybe_add_additional_transports( + context: Context, +) -> Pin + Send>> { + Box::pin(async move { + maybe_add_additional_transports_inner(&context) + .await + .log_err(&context) + .ok(); + }) +} + +async fn maybe_add_additional_transports_inner(context: &Context) -> Result<()> { let now = time(); + let mut transport_added = false; + info!(context, "dbg maybe_add_additional_transports"); // TODO potentially rename housekeeping_mutex let Ok(_housekeeping_lock) = context.housekeeping_mutex.try_lock() else { // Housekeeping or automatic relay management is already running in another thread, do nothing. + info!(context, "dbg skipping because of taken mutex"); return Ok(()); }; if context @@ -31,6 +49,7 @@ pub(crate) async fn maybe_add_additional_transports(context: &Context) -> Result .await? > now.saturating_sub(AUTOMATIC_ADDITION_DEBOUNCE_SECONDS) { + info!(context, "dbg already ran recently"); return Ok(()); } context @@ -43,6 +62,7 @@ pub(crate) async fn maybe_add_additional_transports(context: &Context) -> Result // Using `for` instead of `while` to prevent infinite loop for _ in 0..NUM_TRANSPORTS_TARGET { if context.count_transports().await? >= NUM_TRANSPORTS_TARGET { + info!(context, "dbg target reached"); return Ok(()); } @@ -70,10 +90,12 @@ pub(crate) async fn maybe_add_additional_transports(context: &Context) -> Result ); return Ok(()); }; + info!(context, "dbg from {candidates:?}, chose {domain}"); let param = login_param_from_domain(domain); let res = crate::configure::configure(context, ¶m).await; if res.is_err() { + info!(context, "dbg error {res:?}"); context .sql .execute("UPDATE relay_candidates SET last_tried=?", (now,)) @@ -83,6 +105,13 @@ pub(crate) async fn maybe_add_additional_transports(context: &Context) -> Result // TODO: Decide whether we want to immediately try again with another relay, // if this one failed. If yes, remove the next line. res?; + + transport_added = true; + info!(context, "dbg success"); + } + if transport_added { + info!(context, "dbg restarting"); + restart_io_if_running_boxed(context.clone()).await; } Ok(()) diff --git a/src/configure.rs b/src/configure.rs index 100547a7db..4e400bd58f 100644 --- a/src/configure.rs +++ b/src/configure.rs @@ -223,39 +223,45 @@ impl Context { let removed_transport_id = self .sql .transaction(|transaction| { - let primary_addr = transaction.query_row( - "SELECT value FROM config WHERE keyname='configured_addr'", - (), - |row| { - let addr: String = row.get(0)?; - Ok(addr) - }, - )?; + let primary_addr = transaction + .query_row( + "SELECT value FROM config WHERE keyname='configured_addr'", + (), + |row| { + let addr: String = row.get(0)?; + Ok(addr) + }, + ) + .context("querying addr")?; if primary_addr == addr { bail!("Cannot delete primary transport"); } - let (transport_id, add_timestamp) = transaction.query_row( - "DELETE FROM transports WHERE addr=? RETURNING id, add_timestamp", - (addr,), - |row| { - let id: u32 = row.get(0)?; - let add_timestamp: i64 = row.get(1)?; - Ok((id, add_timestamp)) - }, - )?; + let (transport_id, add_timestamp) = transaction + .query_row( + "DELETE FROM transports WHERE addr=? RETURNING id, add_timestamp", + (addr,), + |row| { + let id: u32 = row.get(0)?; + let add_timestamp: i64 = row.get(1)?; + Ok((id, add_timestamp)) + }, + ) + .with_context(|| format!("Deleting transport, addr: {addr}"))?; // Removal timestamp should not be lower than addition timestamp // to be accepted by other devices when synced. let remove_timestamp = std::cmp::max(now, add_timestamp); - transaction.execute( - "INSERT INTO removed_transports (addr, remove_timestamp) + transaction + .execute( + "INSERT INTO removed_transports (addr, remove_timestamp) VALUES (?, ?) ON CONFLICT (addr) DO UPDATE SET remove_timestamp = excluded.remove_timestamp", - (addr, remove_timestamp), - )?; + (addr, remove_timestamp), + ) + .context("Inserting into removed transport")?; Ok(transport_id) }) diff --git a/src/imap/idle.rs b/src/imap/idle.rs index dd3bb1eab0..bc417bed14 100644 --- a/src/imap/idle.rs +++ b/src/imap/idle.rs @@ -53,10 +53,9 @@ impl Session { // we try to add additional relays right before going into IDLE mode, // because apparently we are connected, but don't have anything important to do. - crate::automatic_transport_management::maybe_add_additional_transports(context) - .await - .context("maybe_add_additional_relays") - .log_err(context); + tokio::task::spawn( + crate::automatic_transport_management::maybe_add_additional_transports(context.clone()), + ); let mut handle = self.inner.idle(); handle diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 3975425eb8..4524b4ce34 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -2547,7 +2547,7 @@ UPDATE msgs SET state=24 WHERE state=18; -- Change OutPreparing to OutFailed. let mut statement = transaction.prepare("INSERT INTO relay_candidates(domain) VALUES (?)")?; for domain in DEFAULT_RELAY_CANDIDATES { - statement.execute((domain,)); + statement.execute((domain,))?; } Ok(()) }, diff --git a/src/transport.rs b/src/transport.rs index 82603e67f2..1ccb5e1fed 100644 --- a/src/transport.rs +++ b/src/transport.rs @@ -831,7 +831,9 @@ pub(crate) async fn sync_transports( /// Same as `context.restart_io_if_running()`, but `Box::pin`ed and with a `+ Send` bound, /// so that it can be called recursively. -fn restart_io_if_running_boxed(context: Context) -> Pin + Send>> { +pub(crate) fn restart_io_if_running_boxed( + context: Context, +) -> Pin + Send>> { Box::pin(async move { context.restart_io_if_running().await }) } From d2ac8e34db10de9125bad6749daa7ba874fe66f7 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Wed, 22 Jul 2026 15:18:16 +0200 Subject: [PATCH 6/8] Fix two bugs, look at the TODOs --- src/automatic_transport_management.rs | 23 ++++++++++++++--------- src/imap/idle.rs | 1 + 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/automatic_transport_management.rs b/src/automatic_transport_management.rs index c15cc43e2d..667a83e8b4 100644 --- a/src/automatic_transport_management.rs +++ b/src/automatic_transport_management.rs @@ -1,14 +1,13 @@ use std::pin::Pin; use anyhow::Result; -use rand::TryRngCore as _; +use deltachat_contact_tools::addr_normalize; use rand::distr::{Alphanumeric, SampleString}; use rand::seq::IndexedRandom; use crate::config::Config; use crate::log::LogExt as _; use crate::login_param::{EnteredCertificateChecks, EnteredImapLoginParam}; -use crate::transport::restart_io_if_running_boxed; use crate::{configure::EnteredLoginParam, context::Context, tools::time}; /// The target number of transports we try to reach. @@ -18,11 +17,7 @@ const AUTOMATIC_ADDITION_DEBOUNCE_SECONDS: i64 = 60 * 60; // one hour /// How long we ignore a transport candidate after failing to create an account there: const BACKOFF_PERIOD_FOR_NOT_WORKING_TRANSPORT: i64 = 60 * 60 * 24 * 7; // one week -// TODO think about how this interacts with stop_io()/start_io() - -// TODO decide if this should be done asynchronously in a task; -// generally we should be able to try adding relays while io is running. -pub(crate) async fn maybe_add_additional_transports( +pub(crate) fn maybe_add_additional_transports( context: Context, ) -> Pin + Send>> { Box::pin(async move { @@ -38,7 +33,6 @@ async fn maybe_add_additional_transports_inner(context: &Context) -> Result<()> let mut transport_added = false; info!(context, "dbg maybe_add_additional_transports"); - // TODO potentially rename housekeeping_mutex let Ok(_housekeeping_lock) = context.housekeeping_mutex.try_lock() else { // Housekeeping or automatic relay management is already running in another thread, do nothing. info!(context, "dbg skipping because of taken mutex"); @@ -52,6 +46,16 @@ async fn maybe_add_additional_transports_inner(context: &Context) -> Result<()> info!(context, "dbg already ran recently"); return Ok(()); } + // TODO uncomment this after I'm done with testing: + // if context + // .get_config_bool(Config::AutomaticTransportManagement) + // .await? + // { + // info!(context, "dbg automatic transport management disabled"); + // return Ok(()); + // } + // 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::LastAutomaticTransportManagement, @@ -111,7 +115,7 @@ async fn maybe_add_additional_transports_inner(context: &Context) -> Result<()> } if transport_added { info!(context, "dbg restarting"); - restart_io_if_running_boxed(context.clone()).await; + context.restart_io_if_running().await; } Ok(()) @@ -121,6 +125,7 @@ pub(crate) fn login_param_from_domain(domain: &str) -> EnteredLoginParam { let rng = &mut rand::rng(); let username = Alphanumeric.sample_string(rng, 9); let addr = username + "@" + domain; + let addr = addr_normalize(&addr); let password = Alphanumeric.sample_string(rng, 50); let param = EnteredLoginParam { diff --git a/src/imap/idle.rs b/src/imap/idle.rs index bc417bed14..d03b4b5ae0 100644 --- a/src/imap/idle.rs +++ b/src/imap/idle.rs @@ -56,6 +56,7 @@ impl Session { tokio::task::spawn( crate::automatic_transport_management::maybe_add_additional_transports(context.clone()), ); + info!(context, "dbg spawned transports task"); let mut handle = self.inner.idle(); handle From 3d44d66d75eacac34c58174e134c605116c48dc0 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Wed, 22 Jul 2026 16:07:03 +0200 Subject: [PATCH 7/8] Reduce password length --- src/automatic_transport_management.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/automatic_transport_management.rs b/src/automatic_transport_management.rs index 667a83e8b4..29545c05d5 100644 --- a/src/automatic_transport_management.rs +++ b/src/automatic_transport_management.rs @@ -126,7 +126,8 @@ pub(crate) fn login_param_from_domain(domain: &str) -> EnteredLoginParam { let username = Alphanumeric.sample_string(rng, 9); let addr = username + "@" + domain; let addr = addr_normalize(&addr); - let password = Alphanumeric.sample_string(rng, 50); + // 22 * log2(26 * 2 + 10) = 130 bits of entropy + let password = Alphanumeric.sample_string(rng, 22); let param = EnteredLoginParam { addr, From e2200296158314b0bd1d0925880926084f7af000 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Wed, 22 Jul 2026 16:08:43 +0200 Subject: [PATCH 8/8] --wip-- [skip ci] --- src/automatic_transport_management.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/automatic_transport_management.rs b/src/automatic_transport_management.rs index 29545c05d5..3c8e923f86 100644 --- a/src/automatic_transport_management.rs +++ b/src/automatic_transport_management.rs @@ -47,13 +47,13 @@ async fn maybe_add_additional_transports_inner(context: &Context) -> Result<()> return Ok(()); } // TODO uncomment this after I'm done with testing: - // if context - // .get_config_bool(Config::AutomaticTransportManagement) - // .await? - // { - // info!(context, "dbg automatic transport management disabled"); - // return Ok(()); - // } + if context + .get_config_bool(Config::AutomaticTransportManagement) + .await? + { + info!(context, "dbg automatic transport management disabled"); + return Ok(()); + } // Set the config at the beginning to avoid endless loops. // Race conditions are not a concern because we locked the mutex. context