mirror of
https://github.com/chatmail/core.git
synced 2026-09-22 13:01:21 +03:00
feat: add init_transports() for multi-relay onboarding
Closes #8693 and supsersedes #8707 from which the API and some overall shape of this commit is inspired. UIs call `init_transports(None)` or `init_transports(qr)` to initialize a first transport on the fresh profile, with more transports added in the background later.
This commit is contained in:
@@ -546,6 +546,18 @@ impl CommandApi {
|
|||||||
ctx.add_transport_from_qr(&qr).await
|
ctx.add_transport_from_qr(&qr).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds an initial transport on a randomly chosen chatmail relay
|
||||||
|
/// and lets the profile add further ones in the background.
|
||||||
|
///
|
||||||
|
/// A `DCACCOUNT:` or `DCLOGIN:` `qr` code adds a single transport
|
||||||
|
/// while securejoin codes add the inviter's relays to the candidates.
|
||||||
|
///
|
||||||
|
/// Does nothing if the profile already has a transport.
|
||||||
|
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.
|
/// 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
|
/// Use [Self::add_or_update_transport()] to add or change a transport
|
||||||
/// and [Self::delete_transport()] to remove a transport.
|
/// and [Self::delete_transport()] to remove a transport.
|
||||||
|
|||||||
@@ -139,6 +139,18 @@ class Account:
|
|||||||
"""Add a new transport using a QR code."""
|
"""Add a new transport using a QR code."""
|
||||||
yield self._rpc.add_transport_from_qr.future(self.id, qr)
|
yield self._rpc.add_transport_from_qr.future(self.id, qr)
|
||||||
|
|
||||||
|
@futuremethod
|
||||||
|
def init_transports(self, qr: Optional[str] = None):
|
||||||
|
"""Add an initial transport on a randomly chosen chatmail relay.
|
||||||
|
|
||||||
|
The profile then adds further ones in the background.
|
||||||
|
A ``DCACCOUNT:`` or ``DCLOGIN:`` ``qr`` code adds a single transport
|
||||||
|
while securejoin codes add the inviter's relays to the candidates.
|
||||||
|
|
||||||
|
Does nothing if the profile already has a transport.
|
||||||
|
"""
|
||||||
|
yield self._rpc.init_transports.future(self.id, qr)
|
||||||
|
|
||||||
def delete_transport(self, addr: str):
|
def delete_transport(self, addr: str):
|
||||||
"""Delete a transport."""
|
"""Delete a transport."""
|
||||||
self._rpc.delete_transport(self.id, addr)
|
self._rpc.delete_transport(self.id, addr)
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ def wait_for_imap_message(imap):
|
|||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_transports(acf):
|
||||||
|
account = acf.get_unconfigured_account()
|
||||||
|
account.init_transports(acf.get_account_qr())
|
||||||
|
assert len(account.list_transports()) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_add_second_address(acf) -> None:
|
def test_add_second_address(acf) -> None:
|
||||||
account = acf.new_configured_account()
|
account = acf.new_configured_account()
|
||||||
assert len(account.list_transports()) == 1
|
assert len(account.list_transports()) == 1
|
||||||
|
|||||||
146
src/autorelay.rs
146
src/autorelay.rs
@@ -1,29 +1,22 @@
|
|||||||
//! # Automatic relay handling (experimental, still in development)
|
//! # Automatic multi-relay onboarding
|
||||||
//!
|
//!
|
||||||
//! Chatmail relays create an account on first login,
|
//! Support for automatically onboarding a profile on transport
|
||||||
//! so a profile can add further transports on its own without user interaction.
|
//! candidates without the user choosing a relay.
|
||||||
//! Candidate hosts come from the `relay_candidates` table,
|
|
||||||
//! 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.
|
|
||||||
|
|
||||||
|
use std::collections::BTreeMap;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::{Result, format_err};
|
||||||
use deltachat_contact_tools::addr_normalize;
|
use deltachat_contact_tools::addr_normalize;
|
||||||
use rand::distr::{Alphanumeric, SampleString};
|
use rand::distr::{Alphanumeric, SampleString};
|
||||||
use rand::seq::IndexedRandom;
|
use rand::seq::{IndexedRandom, SliceRandom};
|
||||||
|
use rusqlite::Transaction;
|
||||||
|
|
||||||
use crate::config::{self, Config};
|
use crate::config::{self, Config};
|
||||||
|
use crate::configure::{EnteredLoginParam, configure};
|
||||||
use crate::log::{LogExt, warn};
|
use crate::log::{LogExt, warn};
|
||||||
use crate::login_param::{EnteredCertificateChecks, EnteredImapLoginParam};
|
use crate::login_param::{EnteredCertificateChecks, EnteredImapLoginParam};
|
||||||
use crate::{configure::EnteredLoginParam, context::Context, tools::time};
|
use crate::{context::Context, tools::time};
|
||||||
|
|
||||||
/// The target number of transports.
|
/// The target number of transports.
|
||||||
const NUM_TRANSPORTS_TARGET: usize = 3;
|
const NUM_TRANSPORTS_TARGET: usize = 3;
|
||||||
@@ -32,6 +25,62 @@ const AUTOMATIC_ADDITION_DEBOUNCE_SECONDS: i64 = 60 * 60; // one hour
|
|||||||
/// How long we ignore a relay candidate after failing to connect to it:
|
/// 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 BACKOFF_PERIOD_FOR_NOT_WORKING_RELAY: i64 = 60 * 60 * 24 * 7; // one week
|
||||||
|
|
||||||
|
/// Sorted relay list a profile can attempt to onboard on without the user choosing one.
|
||||||
|
const DEFAULT_RELAY_CANDIDATES: &[&str] = &[
|
||||||
|
// "chat.adminforge.de", // iroh relay 404s
|
||||||
|
// "chat.feld.me", // iroh relay 404s
|
||||||
|
"chat.me.ke",
|
||||||
|
"chat.nuvon.app",
|
||||||
|
"chat.tinydispatch.org",
|
||||||
|
"chat.vim.wtf",
|
||||||
|
"chatmail.uk",
|
||||||
|
"chtml.ca",
|
||||||
|
"deltachat.me",
|
||||||
|
"e2e.sus.fr",
|
||||||
|
"mailchat.pl",
|
||||||
|
"nchrcht.la10cy.net",
|
||||||
|
"nine.testrun.org",
|
||||||
|
"sweetfern.net",
|
||||||
|
"tarpit.fun",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Records the hosts of `addrs` as relay candidates.
|
||||||
|
pub(crate) async fn add_relay_candidates(context: &Context, addrs: &[String]) -> Result<()> {
|
||||||
|
context
|
||||||
|
.sql
|
||||||
|
.transaction(|tx| hosts_of(addrs).try_for_each(|host| save_relay_candidate(tx, host, 0)))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a first transport on a random relay candidate.
|
||||||
|
pub(crate) async fn add_transport_from_candidates(
|
||||||
|
context: &Context,
|
||||||
|
skip_network: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut candidates = triable_relay_candidates(context, time()).await?;
|
||||||
|
candidates.shuffle(&mut rand::rng());
|
||||||
|
let mut last_err = format_err!("No relay candidates");
|
||||||
|
|
||||||
|
// We patiently try each candidate in turn which might take a while
|
||||||
|
// if many hosts are unreachable but eventually succeeds if one candidate works.
|
||||||
|
let mark_as_autorelay = true;
|
||||||
|
for host in &candidates {
|
||||||
|
let param = login_param_from_host(host, mark_as_autorelay);
|
||||||
|
match configure(context, ¶m, skip_network).await {
|
||||||
|
Ok(()) => {
|
||||||
|
param.save_legacy(context).await?;
|
||||||
|
info!(context, "Added a transport on relay {host}.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(context, "Failed to add relay {host}: {err:#}.");
|
||||||
|
last_err = err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(last_err)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn maybe_add_additional_relays(
|
pub(crate) fn maybe_add_additional_relays(
|
||||||
context: Context,
|
context: Context,
|
||||||
) -> Pin<Box<dyn Future<Output = ()> + Send>> {
|
) -> Pin<Box<dyn Future<Output = ()> + Send>> {
|
||||||
@@ -93,10 +142,7 @@ async fn maybe_add_additional_relays_inner(context: &Context, skip_network: bool
|
|||||||
return Ok(relay_added);
|
return Ok(relay_added);
|
||||||
}
|
}
|
||||||
|
|
||||||
// First, query all candidates that were not tried since `BACKOFF_PERIOD_FOR_NOT_WORKING_RELAY` seconds.
|
let candidates = triable_relay_candidates(context, now).await?;
|
||||||
// Hosts that are already used are excluded.
|
|
||||||
let candidates = load_relay_candidates(context, now).await?;
|
|
||||||
|
|
||||||
let Some(host) = candidates.choose(&mut rand::rng()) else {
|
let Some(host) = candidates.choose(&mut rand::rng()) else {
|
||||||
info!(
|
info!(
|
||||||
context,
|
context,
|
||||||
@@ -113,14 +159,11 @@ async fn maybe_add_additional_relays_inner(context: &Context, skip_network: bool
|
|||||||
|
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.transaction(|tx| save_relay_candidate(tx, host, now))
|
||||||
"UPDATE relay_candidates SET last_tried=? WHERE host=?",
|
|
||||||
(now, host),
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
let mark_as_autorelay = true;
|
let mark_as_autorelay = true;
|
||||||
let param = login_param_from_host(host, mark_as_autorelay);
|
let param = login_param_from_host(host, mark_as_autorelay);
|
||||||
let res = crate::configure::configure(context, ¶m, skip_network).await;
|
let res = configure(context, ¶m, skip_network).await;
|
||||||
if let Err(e) = res {
|
if let Err(e) = res {
|
||||||
warn!(
|
warn!(
|
||||||
context,
|
context,
|
||||||
@@ -135,30 +178,49 @@ async fn maybe_add_additional_relays_inner(context: &Context, skip_network: bool
|
|||||||
Ok(relay_added)
|
Ok(relay_added)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load_relay_candidates(context: &Context, now: i64) -> Result<Vec<String>, anyhow::Error> {
|
async fn triable_relay_candidates(context: &Context, now: i64) -> Result<Vec<String>> {
|
||||||
let cutoff_timestamp = now.saturating_sub(BACKOFF_PERIOD_FOR_NOT_WORKING_RELAY);
|
let cutoff_timestamp = now.saturating_sub(BACKOFF_PERIOD_FOR_NOT_WORKING_RELAY);
|
||||||
let candidates: Vec<String> = context
|
let mut last_tried: BTreeMap<String, i64> = context
|
||||||
.sql
|
.sql
|
||||||
.query_map_vec(
|
.query_map_collect("SELECT host, last_tried FROM relay_candidates", (), |row| {
|
||||||
// This also selects candidates which have last_tried in the future,
|
Ok((row.get(0)?, row.get(1)?))
|
||||||
// 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?;
|
.await?;
|
||||||
|
for host in DEFAULT_RELAY_CANDIDATES {
|
||||||
|
last_tried.entry(host.to_string()).or_insert(0);
|
||||||
|
}
|
||||||
|
let self_addrs = context.get_self_addrs().await?;
|
||||||
|
let used_hosts: Vec<&str> = hosts_of(&self_addrs).collect();
|
||||||
|
|
||||||
|
// We also try candidates which have `last_tried` in the future,
|
||||||
|
// which on next failure get `last_tried` reset to the current time.
|
||||||
|
let candidates = last_tried
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(host, last_tried)| {
|
||||||
|
(*last_tried < cutoff_timestamp || *last_tried > now)
|
||||||
|
&& !used_hosts.contains(&host.as_str())
|
||||||
|
})
|
||||||
|
.map(|(host, _)| host)
|
||||||
|
.collect();
|
||||||
|
|
||||||
Ok(candidates)
|
Ok(candidates)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the host of each address in `addrs`.
|
||||||
|
fn hosts_of(addrs: &[String]) -> impl Iterator<Item = &str> {
|
||||||
|
addrs.iter().filter_map(|a| Some(a.rsplit_once('@')?.1))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Records `host` as a relay candidate, overwriting a stored `last_tried`.
|
||||||
|
fn save_relay_candidate(tx: &Transaction, host: &str, last_tried: i64) -> Result<()> {
|
||||||
|
tx.execute(
|
||||||
|
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)
|
||||||
|
ON CONFLICT(host) DO UPDATE SET last_tried=excluded.last_tried",
|
||||||
|
(host, last_tried),
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn login_param_from_host(host: &str, mark_as_autorelay: bool) -> EnteredLoginParam {
|
pub(crate) fn login_param_from_host(host: &str, mark_as_autorelay: bool) -> EnteredLoginParam {
|
||||||
let rng = &mut rand::rng();
|
let rng = &mut rand::rng();
|
||||||
let username = Alphanumeric.sample_string(rng, 9);
|
let username = Alphanumeric.sample_string(rng, 9);
|
||||||
|
|||||||
@@ -1,42 +1,144 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::test_utils::TestContext;
|
use crate::test_utils::{TestContext, TestContextManager};
|
||||||
use crate::tools::SystemTime;
|
use crate::tools::SystemTime;
|
||||||
|
|
||||||
|
/// Tests that the default relays are candidates without a row in the table.
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn test_load_relay_candidates_single() -> Result<()> {
|
async fn test_triable_relay_candidates_defaults() -> Result<()> {
|
||||||
|
let mut tcm = TestContextManager::new();
|
||||||
|
let t = &tcm.unconfigured().await;
|
||||||
|
let now = time();
|
||||||
|
|
||||||
|
assert!(DEFAULT_RELAY_CANDIDATES.is_sorted());
|
||||||
|
let mut candidates = triable_relay_candidates(t, now).await?;
|
||||||
|
candidates.sort();
|
||||||
|
assert_eq!(candidates, DEFAULT_RELAY_CANDIDATES);
|
||||||
|
|
||||||
|
let tried = DEFAULT_RELAY_CANDIDATES[0];
|
||||||
|
save_relay_candidates(t, &[tried], now).await?;
|
||||||
|
let candidates = triable_relay_candidates(t, now).await?;
|
||||||
|
assert_eq!(candidates.len(), DEFAULT_RELAY_CANDIDATES.len() - 1);
|
||||||
|
assert!(!candidates.contains(&tried.to_string()));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests that a transport is added on a candidate from the given addresses.
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_add_transport_from_candidates() -> Result<()> {
|
||||||
|
let mut tcm = TestContextManager::new();
|
||||||
|
let t = &tcm.unconfigured().await;
|
||||||
|
mark_defaults_tried(t, time()).await?;
|
||||||
|
let addrs_from_qr = [
|
||||||
|
"alice@example.org".to_string(),
|
||||||
|
"bob@example.org".to_string(),
|
||||||
|
];
|
||||||
|
let skip_network = true;
|
||||||
|
add_relay_candidates(t, &addrs_from_qr).await?;
|
||||||
|
add_transport_from_candidates(t, skip_network).await?;
|
||||||
|
|
||||||
|
let transports = t.list_transports().await?;
|
||||||
|
assert_eq!(transports.len(), 1);
|
||||||
|
assert!(transports[0].addr.ends_with("@example.org"));
|
||||||
|
assert_eq!(
|
||||||
|
t.get_config(Config::Addr).await?,
|
||||||
|
Some(transports[0].addr.clone())
|
||||||
|
);
|
||||||
|
let untried = untried_relay_candidates(t).await?;
|
||||||
|
assert_eq!(untried, ["example.org"]);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests correct add_transport_from_candidates error handling.
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_add_transport_from_candidates_failure() -> Result<()> {
|
||||||
|
let mut tcm = TestContextManager::new();
|
||||||
|
let t = &tcm.unconfigured().await;
|
||||||
|
mark_defaults_tried(t, time()).await?;
|
||||||
|
save_relay_candidates(t, &["bad host", "worse host"], 0).await?;
|
||||||
|
|
||||||
|
let skip_network = false;
|
||||||
|
let err = add_transport_from_candidates(t, skip_network)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(format!("{err:#}").contains("Bad email-address"));
|
||||||
|
assert!(!t.is_configured().await?);
|
||||||
|
let untried = untried_relay_candidates(t).await?;
|
||||||
|
assert_eq!(untried, ["bad host", "worse host"]);
|
||||||
|
t.assert_warns_or_errors(&[
|
||||||
|
"Failed to add relay bad host",
|
||||||
|
"Failed to add relay worse host",
|
||||||
|
])
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn untried_relay_candidates(t: &TestContext) -> Result<Vec<String>> {
|
||||||
|
t.sql
|
||||||
|
.query_map_vec(
|
||||||
|
"SELECT host FROM relay_candidates WHERE last_tried=0 ORDER BY host",
|
||||||
|
(),
|
||||||
|
|row| Ok(row.get(0)?),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save_relay_candidates(t: &TestContext, hosts: &[&str], last_tried: i64) -> Result<()> {
|
||||||
|
t.sql
|
||||||
|
.transaction(|tx| {
|
||||||
|
for host in hosts {
|
||||||
|
save_relay_candidate(tx, host, last_tried)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Keeps the default relays out of `triable_relay_candidates()`.
|
||||||
|
async fn mark_defaults_tried(t: &TestContext, now: i64) -> Result<()> {
|
||||||
|
save_relay_candidates(t, DEFAULT_RELAY_CANDIDATES, now).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests that saving a candidate overwrites its stored timestamp.
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_save_relay_candidate() -> Result<()> {
|
||||||
|
let mut tcm = TestContextManager::new();
|
||||||
|
let t = &tcm.unconfigured().await;
|
||||||
|
let now = time();
|
||||||
|
|
||||||
|
for last_tried in [0, now, 0] {
|
||||||
|
t.sql
|
||||||
|
.transaction(|tx| save_relay_candidate(tx, "relay.example", last_tried))
|
||||||
|
.await?;
|
||||||
|
let stored: Option<i64> = t
|
||||||
|
.sql
|
||||||
|
.query_get_value(
|
||||||
|
"SELECT last_tried FROM relay_candidates WHERE host=?",
|
||||||
|
("relay.example",),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(stored, Some(last_tried));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_triable_relay_candidates_single() -> Result<()> {
|
||||||
let t = &TestContext::new_alice().await;
|
let t = &TestContext::new_alice().await;
|
||||||
enable_config(t).await;
|
enable_config(t).await;
|
||||||
let now = time();
|
let now = time();
|
||||||
|
|
||||||
t.sql.execute("DELETE FROM relay_candidates", ()).await?;
|
mark_defaults_tried(t, now).await?;
|
||||||
|
|
||||||
// This host should be returned by load_relay_candidates():
|
save_relay_candidates(t, &["never_tried.example", "example.org"], 0).await?;
|
||||||
t.sql
|
save_relay_candidates(t, &["recent.example"], now).await?;
|
||||||
.execute(
|
|
||||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
|
||||||
("never_tried.example", 0),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// This host was recently tried and should not be returned:
|
let candidates = triable_relay_candidates(t, now).await?;
|
||||||
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()]);
|
assert_eq!(candidates, vec!["never_tried.example".to_string()]);
|
||||||
|
|
||||||
@@ -44,22 +146,15 @@ async fn test_load_relay_candidates_single() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn test_load_relay_candidates_multiple() -> Result<()> {
|
async fn test_triable_relay_candidates_multiple() -> Result<()> {
|
||||||
let t = &TestContext::new().await;
|
let t = &TestContext::new().await;
|
||||||
enable_config(t).await;
|
enable_config(t).await;
|
||||||
let now = time();
|
let now = time();
|
||||||
|
|
||||||
t.sql.execute("DELETE FROM relay_candidates", ()).await?;
|
mark_defaults_tried(t, now).await?;
|
||||||
for host in ["a.example", "b.example", "c.example"] {
|
save_relay_candidates(t, &["a.example", "b.example", "c.example"], 0).await?;
|
||||||
t.sql
|
|
||||||
.execute(
|
|
||||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
|
||||||
(host, 0),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut candidates = load_relay_candidates(t, now).await?;
|
let mut candidates = triable_relay_candidates(t, now).await?;
|
||||||
candidates.sort();
|
candidates.sort();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -160,13 +255,8 @@ async fn test_maybe_add_additional_relays_add_one() -> Result<()> {
|
|||||||
enable_config(t).await;
|
enable_config(t).await;
|
||||||
let now = time();
|
let now = time();
|
||||||
|
|
||||||
t.sql.execute("DELETE FROM relay_candidates", ()).await?;
|
mark_defaults_tried(t, now).await?;
|
||||||
t.sql
|
save_relay_candidates(t, &["relay.example"], 0).await?;
|
||||||
.execute(
|
|
||||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
|
||||||
("relay.example", 0),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let transports_before = t.count_transports().await?;
|
let transports_before = t.count_transports().await?;
|
||||||
|
|
||||||
@@ -189,15 +279,8 @@ async fn test_maybe_add_additional_relays_add_multiple() -> Result<()> {
|
|||||||
enable_config(t).await;
|
enable_config(t).await;
|
||||||
let now = time();
|
let now = time();
|
||||||
|
|
||||||
t.sql.execute("DELETE FROM relay_candidates", ()).await?;
|
mark_defaults_tried(t, now).await?;
|
||||||
for host in ["a.example", "b.example", "c.example", "d.example"] {
|
save_relay_candidates(t, &["a.example", "b.example", "c.example", "d.example"], 0).await?;
|
||||||
t.sql
|
|
||||||
.execute(
|
|
||||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
|
||||||
(host, 0),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let skip_network = true;
|
let skip_network = true;
|
||||||
let relay_added = maybe_add_additional_relays_inner(t, skip_network).await?;
|
let relay_added = maybe_add_additional_relays_inner(t, skip_network).await?;
|
||||||
@@ -218,14 +301,9 @@ async fn test_maybe_add_additional_relays_failure() -> Result<()> {
|
|||||||
enable_config(t).await;
|
enable_config(t).await;
|
||||||
let now = time();
|
let now = time();
|
||||||
|
|
||||||
t.sql.execute("DELETE FROM relay_candidates", ()).await?;
|
mark_defaults_tried(t, now).await?;
|
||||||
for i in 1..10 {
|
for i in 1..10 {
|
||||||
t.sql
|
save_relay_candidates(t, &[format!("{i}.invalid.example").as_str()], 0).await?;
|
||||||
.execute(
|
|
||||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
|
||||||
(format!("{i}.invalid.example"), 0),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let transports_before = t.count_transports().await?;
|
let transports_before = t.count_transports().await?;
|
||||||
@@ -254,7 +332,7 @@ async fn test_maybe_add_additional_relays_failure() -> Result<()> {
|
|||||||
|
|
||||||
// ...but not all, because there might be many relay candidates
|
// ...but not all, because there might be many relay candidates
|
||||||
// and we don't want to try all of them in a single call:
|
// 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);
|
assert_eq!(triable_relay_candidates(t, now).await?.is_empty(), false);
|
||||||
|
|
||||||
t.assert_warns_or_errors(&[
|
t.assert_warns_or_errors(&[
|
||||||
"DNS lookup with memory cache failure",
|
"DNS lookup with memory cache failure",
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ use crate::login_param::EnteredCertificateChecks;
|
|||||||
pub use crate::login_param::EnteredLoginParam;
|
pub use crate::login_param::EnteredLoginParam;
|
||||||
use crate::net::proxy::ProxyConfig;
|
use crate::net::proxy::ProxyConfig;
|
||||||
use crate::provider::{self, Protocol, Socket};
|
use crate::provider::{self, Protocol, Socket};
|
||||||
use crate::qr::{login_param_from_account_qr, login_param_from_login_qr};
|
use crate::qr::{Qr, check_qr, login_param_from_account_qr, login_param_from_login_qr};
|
||||||
use crate::smtp::Smtp;
|
use crate::smtp::Smtp;
|
||||||
use crate::sync::Sync::Nosync;
|
use crate::sync::Sync::Nosync;
|
||||||
use crate::tools::time;
|
use crate::tools::time;
|
||||||
@@ -40,7 +40,7 @@ use crate::transport::{
|
|||||||
ConnectionCandidate, delete_transport_row, maybe_update_sending_transport,
|
ConnectionCandidate, delete_transport_row, maybe_update_sending_transport,
|
||||||
purge_transport_caches, send_sync_transports, transport_addrs,
|
purge_transport_caches, send_sync_transports, transport_addrs,
|
||||||
};
|
};
|
||||||
use crate::{EventType, stock_str};
|
use crate::{EventType, autorelay, stock_str};
|
||||||
|
|
||||||
/// Maximum number of relays.
|
/// Maximum number of relays.
|
||||||
///
|
///
|
||||||
@@ -190,6 +190,59 @@ impl Context {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds an initial transport on a randomly chosen chatmail relay
|
||||||
|
/// and lets the profile add further ones in the background.
|
||||||
|
///
|
||||||
|
/// A `DCACCOUNT:` or `DCLOGIN:` `qr` code adds a single transport
|
||||||
|
/// while securejoin codes add the inviter's relays to the candidates.
|
||||||
|
///
|
||||||
|
/// Does nothing if the profile already has a transport.
|
||||||
|
pub async fn init_transports(&self, qr: Option<&str>) -> Result<()> {
|
||||||
|
if self.is_configured().await? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if let Some(qr) = qr {
|
||||||
|
match check_qr(self, qr).await? {
|
||||||
|
Qr::Account { .. } | Qr::Login { .. } => {
|
||||||
|
return self.add_transport_from_qr(qr).await;
|
||||||
|
}
|
||||||
|
Qr::AskVerifyContact { addrs, .. }
|
||||||
|
| Qr::AskVerifyGroup { addrs, .. }
|
||||||
|
| Qr::AskJoinBroadcast { addrs, .. } => {
|
||||||
|
autorelay::add_relay_candidates(self, &addrs).await?
|
||||||
|
}
|
||||||
|
_ => bail!("QR code does not contain a relay"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancel_channel = self.alloc_ongoing().await?;
|
||||||
|
let skip_network = false;
|
||||||
|
let res = autorelay::add_transport_from_candidates(self, skip_network)
|
||||||
|
.race(cancel_channel.recv().map(|_| Err(format_err!("Canceled"))))
|
||||||
|
.await;
|
||||||
|
self.free_ongoing().await;
|
||||||
|
|
||||||
|
let configured = self.is_configured().await?;
|
||||||
|
match res {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(err) if configured => {
|
||||||
|
warn!(
|
||||||
|
self,
|
||||||
|
"Onboarding interrupted after adding a transport: {err:#}."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let error_msg = stock_str::configuration_failed(self, &format!("{err:#}"));
|
||||||
|
progress!(self, 0, Some(error_msg.clone()));
|
||||||
|
bail!(error_msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.set_config_bool(Config::Autorelay, true).await?;
|
||||||
|
progress!(self, 1000);
|
||||||
|
self.start_io().await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the list of all email accounts that are used as a transport in the current profile.
|
/// 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
|
/// Use [Self::add_or_update_transport()] to add or change a transport
|
||||||
/// and [Self::delete_transport()] to delete a transport.
|
/// and [Self::delete_transport()] to delete a transport.
|
||||||
@@ -690,6 +743,26 @@ mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tests init_transports on a bad code and on a configured profile.
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_init_transports() -> Result<()> {
|
||||||
|
let mut tcm = TestContextManager::new();
|
||||||
|
let t = &tcm.unconfigured().await;
|
||||||
|
assert!(t.init_transports(Some("not a qr code")).await.is_err());
|
||||||
|
assert!(!t.is_configured().await?);
|
||||||
|
|
||||||
|
let alice = &tcm.alice().await;
|
||||||
|
let invite = "openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40invite.example&i=TbnwJ6lSvD5&s=0ejvbdFSQxB";
|
||||||
|
alice.init_transports(Some(invite)).await?;
|
||||||
|
let candidates = alice
|
||||||
|
.sql
|
||||||
|
.count("SELECT COUNT(*) FROM relay_candidates", ())
|
||||||
|
.await?;
|
||||||
|
assert_eq!(candidates, 0);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn test_get_configured_param() -> Result<()> {
|
async fn test_get_configured_param() -> Result<()> {
|
||||||
let t = &TestContext::new().await;
|
let t = &TestContext::new().await;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use serde::Deserialize;
|
|||||||
|
|
||||||
use crate::autorelay::login_param_from_host;
|
use crate::autorelay::login_param_from_host;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
use crate::configure::MAX_RELAYS;
|
||||||
use crate::contact::{Contact, ContactId, Origin};
|
use crate::contact::{Contact, ContactId, Origin};
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::key::Fingerprint;
|
use crate::key::Fingerprint;
|
||||||
@@ -500,7 +501,8 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
|||||||
addrs.push(normalize_address(primary_addr)?);
|
addrs.push(normalize_address(primary_addr)?);
|
||||||
};
|
};
|
||||||
if let Some(secondary_addrs_raw) = param.get("r") {
|
if let Some(secondary_addrs_raw) = param.get("r") {
|
||||||
for secondary_address in secondary_addrs_raw.split(',') {
|
let max_secondary = MAX_RELAYS.saturating_sub(addrs.len());
|
||||||
|
for secondary_address in secondary_addrs_raw.split(',').take(max_secondary) {
|
||||||
addrs.push(normalize_address(secondary_address)?)
|
addrs.push(normalize_address(secondary_address)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -361,6 +361,23 @@ async fn test_decode_openpgp_secure_join() -> Result<()> {
|
|||||||
bail!("Wrong QR code type");
|
bail!("Wrong QR code type");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A bad invite code must not be able to steer us onto arbitrarily many relays.
|
||||||
|
let relays: Vec<String> = (0..20).map(|i| format!("cli%40r{i}.example.org")).collect();
|
||||||
|
let qr = check_qr(
|
||||||
|
&ctx.ctx,
|
||||||
|
&format!(
|
||||||
|
"openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&r={}&i=TbnwJ6lSvD5&s=0ejvbdFSQxB",
|
||||||
|
relays.join(",")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Qr::AskVerifyContact { addrs, .. } = qr {
|
||||||
|
assert_eq!(addrs.len(), MAX_RELAYS);
|
||||||
|
} else {
|
||||||
|
bail!("Wrong QR code type");
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2673,6 +2673,13 @@ CREATE TABLE smtp2 (
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inc_and_check(&mut migration_version, 167)?;
|
||||||
|
if dbversion < migration_version {
|
||||||
|
// The default relay candidates are no longer seeded into the table.
|
||||||
|
sql.execute_migration("DELETE FROM relay_candidates", 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