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?