mirror of
https://github.com/chatmail/core.git
synced 2026-09-22 13:01:21 +03:00
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
This commit is contained in:
178
src/automatic_relay_management.rs
Normal file
178
src/automatic_relay_management.rs
Normal file
@@ -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<Box<dyn Future<Output = ()> + 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<bool> {
|
||||||
|
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<Vec<String>, anyhow::Error> {
|
||||||
|
let cutoff_timestamp = now.saturating_sub(BACKOFF_PERIOD_FOR_NOT_WORKING_RELAY);
|
||||||
|
let candidates: Vec<String> = 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<? OR 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;
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -349,6 +349,15 @@ pub enum Config {
|
|||||||
/// Timestamp of the last `CantDecryptOutgoingMsgs` notification.
|
/// Timestamp of the last `CantDecryptOutgoingMsgs` notification.
|
||||||
LastCantDecryptOutgoingMsgs,
|
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.
|
/// Whether to avoid using IMAP IDLE even if the server supports it.
|
||||||
///
|
///
|
||||||
/// This is a developer option for testing "fake idle".
|
/// This is a developer option for testing "fake idle".
|
||||||
|
|||||||
137
src/configure.rs
137
src/configure.rs
@@ -45,21 +45,6 @@ use crate::{EventType, stock_str};
|
|||||||
/// See <https://github.com/chatmail/core/issues/7608>.
|
/// See <https://github.com/chatmail/core/issues/7608>.
|
||||||
pub(crate) const MAX_RELAYS: usize = 5;
|
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 {
|
macro_rules! progress {
|
||||||
($context:tt, $progress:expr, $comment:expr) => {
|
($context:tt, $progress:expr, $comment:expr) => {
|
||||||
assert!(
|
assert!(
|
||||||
@@ -335,9 +320,10 @@ impl Context {
|
|||||||
self.try_make_space_for_new_relay().await?;
|
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
|
// 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!(
|
warn!(
|
||||||
self,
|
self,
|
||||||
"configure failed: Entered params: {}. Used params: {}. Error: {error}.",
|
"configure failed: Entered params: {}. Used params: {}. Error: {error}.",
|
||||||
@@ -401,6 +387,7 @@ impl Context {
|
|||||||
async fn get_configured_param(
|
async fn get_configured_param(
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
param: &EnteredLoginParam,
|
param: &EnteredLoginParam,
|
||||||
|
skip_network: bool,
|
||||||
) -> Result<ConfiguredLoginParam> {
|
) -> Result<ConfiguredLoginParam> {
|
||||||
ensure!(!param.addr.is_empty(), "Missing email address.");
|
ensure!(!param.addr.is_empty(), "Missing email address.");
|
||||||
|
|
||||||
@@ -428,6 +415,7 @@ async fn get_configured_param(
|
|||||||
&& param.smtp.port == 0
|
&& param.smtp.port == 0
|
||||||
&& param.smtp.security == Socket::Automatic
|
&& param.smtp.security == Socket::Automatic
|
||||||
&& param.smtp.user.is_empty()
|
&& param.smtp.user.is_empty()
|
||||||
|
&& !skip_network
|
||||||
{
|
{
|
||||||
// No advanced parameters entered by the user:
|
// No advanced parameters entered by the user:
|
||||||
// do Autoconfig unless the domain has hard-coded legacy servers.
|
// do Autoconfig unless the domain has hard-coded legacy servers.
|
||||||
@@ -528,74 +516,82 @@ async fn get_configured_param(
|
|||||||
Ok(configured_login_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);
|
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 proxy_config = ProxyConfig::load(ctx).await?;
|
||||||
let strict_tls = configured_param.strict_tls(proxy_config.is_some())?;
|
let strict_tls = configured_param.strict_tls(proxy_config.is_some())?;
|
||||||
|
|
||||||
progress!(ctx, 550);
|
progress!(ctx, 550);
|
||||||
|
|
||||||
// Spawn SMTP configuration task
|
if !skip_network {
|
||||||
// to try SMTP while connecting to IMAP.
|
// Spawn SMTP configuration task
|
||||||
let context_smtp = ctx.clone();
|
// to try SMTP while connecting to IMAP.
|
||||||
let smtp_param = configured_param.smtp.clone();
|
let context_smtp = ctx.clone();
|
||||||
let smtp_password = configured_param.smtp_password.clone();
|
let smtp_param = configured_param.smtp.clone();
|
||||||
let smtp_addr = configured_param.addr.clone();
|
let smtp_password = configured_param.smtp_password.clone();
|
||||||
|
let smtp_addr = configured_param.addr.clone();
|
||||||
|
|
||||||
let proxy_config2 = proxy_config.clone();
|
let proxy_config2 = proxy_config.clone();
|
||||||
let smtp_config_task = task::spawn(async move {
|
let smtp_config_task = task::spawn(async move {
|
||||||
let mut smtp = Smtp::new();
|
let mut smtp = Smtp::new();
|
||||||
smtp.connect(
|
smtp.connect(
|
||||||
&context_smtp,
|
&context_smtp,
|
||||||
&smtp_param,
|
&smtp_param,
|
||||||
&smtp_password,
|
&smtp_password,
|
||||||
&proxy_config2,
|
&proxy_config2,
|
||||||
&smtp_addr,
|
&smtp_addr,
|
||||||
strict_tls,
|
strict_tls,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok::<(), anyhow::Error>(())
|
Ok::<(), anyhow::Error>(())
|
||||||
});
|
});
|
||||||
|
|
||||||
progress!(ctx, 600);
|
progress!(ctx, 600);
|
||||||
|
|
||||||
// Configure IMAP
|
// Configure IMAP
|
||||||
|
|
||||||
let transport_id = 0;
|
let transport_id = 0;
|
||||||
let (_s, r) = async_channel::bounded(1);
|
let (_s, r) = async_channel::bounded(1);
|
||||||
let mut imap = Imap::new(ctx, transport_id, configured_param.clone(), r).await?;
|
let mut imap = Imap::new(ctx, transport_id, configured_param.clone(), r).await?;
|
||||||
let configuring = true;
|
let configuring = true;
|
||||||
let imap_session = match imap.connect(ctx, configuring).await {
|
let imap_session = match imap.connect(ctx, configuring).await {
|
||||||
Ok(imap_session) => imap_session,
|
Ok(imap_session) => imap_session,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
bail!("{}", nicer_configuration_error(ctx, format!("{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);
|
// Drop the imap connection explicitly
|
||||||
|
// to make sure that it's not forgotten in a future refactoring
|
||||||
// Wait for SMTP configuration
|
drop(imap_session);
|
||||||
smtp_config_task.await??;
|
drop(imap);
|
||||||
|
|
||||||
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(imap_session);
|
|
||||||
drop(imap);
|
|
||||||
|
|
||||||
progress!(ctx, 910);
|
progress!(ctx, 910);
|
||||||
|
|
||||||
configured_param
|
configured_param
|
||||||
@@ -779,7 +775,8 @@ mod tests {
|
|||||||
|
|
||||||
..Default::default()
|
..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.imap_user, "alice@example.net");
|
||||||
assert_eq!(configured_param.smtp_user, "");
|
assert_eq!(configured_param.smtp_user, "");
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -232,8 +232,8 @@ pub struct InnerContext {
|
|||||||
running_state: RwLock<RunningState>,
|
running_state: RwLock<RunningState>,
|
||||||
/// Mutex to prevent a race condition when a "your pw is wrong" warning is sent, resulting in multiple messages being sent.
|
/// 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<()>,
|
pub(crate) wrong_pw_warning_mutex: Mutex<()>,
|
||||||
/// Mutex to prevent running housekeeping from multiple threads at once.
|
/// Mutex to prevent running housekeeping or relay management from multiple threads at once.
|
||||||
pub(crate) housekeeping_mutex: Mutex<()>,
|
pub(crate) background_task_mutex: Mutex<()>,
|
||||||
|
|
||||||
/// Mutex to prevent multiple IMAP loops from fetching the messages at once.
|
/// Mutex to prevent multiple IMAP loops from fetching the messages at once.
|
||||||
///
|
///
|
||||||
@@ -478,7 +478,7 @@ impl Context {
|
|||||||
running_state: RwLock::new(Default::default()),
|
running_state: RwLock::new(Default::default()),
|
||||||
sql: Sql::new(dbfile),
|
sql: Sql::new(dbfile),
|
||||||
wrong_pw_warning_mutex: Mutex::new(()),
|
wrong_pw_warning_mutex: Mutex::new(()),
|
||||||
housekeeping_mutex: Mutex::new(()),
|
background_task_mutex: Mutex::new(()),
|
||||||
fetch_msgs_mutex: Mutex::new(()),
|
fetch_msgs_mutex: Mutex::new(()),
|
||||||
translated_stockstrings: stockstrings,
|
translated_stockstrings: stockstrings,
|
||||||
events,
|
events,
|
||||||
@@ -1033,6 +1033,24 @@ impl Context {
|
|||||||
.await?
|
.await?
|
||||||
.to_string(),
|
.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);
|
let elapsed = time_elapsed(&self.creation_time);
|
||||||
res.insert("uptime", duration_to_str(elapsed));
|
res.insert("uptime", duration_to_str(elapsed));
|
||||||
|
|||||||
@@ -51,6 +51,12 @@ impl Session {
|
|||||||
return Ok(self);
|
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();
|
let mut handle = self.inner.idle();
|
||||||
handle
|
handle
|
||||||
.init()
|
.init()
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ pub(crate) mod events;
|
|||||||
pub use events::*;
|
pub use events::*;
|
||||||
|
|
||||||
mod aheader;
|
mod aheader;
|
||||||
|
mod automatic_relay_management;
|
||||||
pub mod blob;
|
pub mod blob;
|
||||||
pub mod calls;
|
pub mod calls;
|
||||||
pub mod chat;
|
pub mod chat;
|
||||||
|
|||||||
19
src/qr.rs
19
src/qr.rs
@@ -9,10 +9,9 @@ pub use dclogin_scheme::LoginOptions;
|
|||||||
pub(crate) use dclogin_scheme::login_param_from_login_qr;
|
pub(crate) use dclogin_scheme::login_param_from_login_qr;
|
||||||
use deltachat_contact_tools::{ContactAddress, addr_normalize, may_be_valid_addr};
|
use deltachat_contact_tools::{ContactAddress, addr_normalize, may_be_valid_addr};
|
||||||
use percent_encoding::{NON_ALPHANUMERIC, percent_decode_str, percent_encode};
|
use percent_encoding::{NON_ALPHANUMERIC, percent_decode_str, percent_encode};
|
||||||
use rand::TryRngCore as _;
|
|
||||||
use rand::distr::{Alphanumeric, SampleString};
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::automatic_relay_management::login_param_from_host;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::contact::{Contact, ContactId, Origin};
|
use crate::contact::{Contact, ContactId, Origin};
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
@@ -828,21 +827,7 @@ pub(crate) async fn login_param_from_account_qr(
|
|||||||
.context("Invalid DCACCOUNT scheme")?;
|
.context("Invalid DCACCOUNT scheme")?;
|
||||||
|
|
||||||
if !payload.starts_with(HTTPS_SCHEME) {
|
if !payload.starts_with(HTTPS_SCHEME) {
|
||||||
let rng = &mut rand::rngs::OsRng.unwrap_err();
|
let param = login_param_from_host(payload);
|
||||||
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,
|
|
||||||
};
|
|
||||||
return Ok(param);
|
return Ok(param);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -780,7 +780,7 @@ async fn incremental_vacuum(context: &Context) -> Result<()> {
|
|||||||
|
|
||||||
/// Cleanup the account to restore some storage and optimize the database.
|
/// Cleanup the account to restore some storage and optimize the database.
|
||||||
pub async fn housekeeping(context: &Context) -> Result<()> {
|
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.
|
// Housekeeping is already running in another thread, do nothing.
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2534,6 +2534,39 @@ UPDATE msgs SET state=24 WHERE state=18; -- Change OutPreparing to OutFailed.
|
|||||||
.await?;
|
.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
|
let new_version = sql
|
||||||
.get_raw_config_int(VERSION_CFG)
|
.get_raw_config_int(VERSION_CFG)
|
||||||
.await?
|
.await?
|
||||||
|
|||||||
Reference in New Issue
Block a user