Compare commits

...
Author SHA1 Message Date
link2xt f25c3ac006 WIP: remove maybe_update_sending_transport and ConfiguredAddr 2026-10-10 19:18:40 +00:00
30 changed files with 119 additions and 440 deletions
+2 -5
View File
@@ -31,7 +31,7 @@ use deltachat::internals_for_benches::create_broadcast_secret;
use deltachat::internals_for_benches::save_broadcast_secret;
use deltachat::securejoin::get_securejoin_qr;
use deltachat::{
Events, chat::ChatId, config::Config, context::Context, internals_for_benches::key_from_asc,
Events, chat::ChatId, context::Context, internals_for_benches::key_from_asc,
internals_for_benches::parse_and_get_text, internals_for_benches::store_self_keypair,
stock_str::StockStrings,
};
@@ -57,10 +57,7 @@ async fn create_context() -> Context {
.await
.unwrap();
context
.set_config(Config::ConfiguredAddr, Some("bob@example.net"))
.await
.unwrap();
// FIXME add_pseudo_transport
let secret = key_from_asc(include_str!("../test-data/key/bob-secret.asc")).unwrap();
store_self_keypair(&context, &secret)
.await
+1 -4
View File
@@ -110,11 +110,8 @@ async fn create_context() -> Context {
}
let addr = "alice@example.com";
// FIXME add_pseudo_transport()
context.set_config(Config::Addr, Some(addr)).await.unwrap();
context
.set_config(Config::ConfiguredAddr, Some(addr))
.await
.unwrap();
context
.set_config(Config::Configured, Some("1"))
.await
@@ -104,7 +104,6 @@ def test_keyupdate_against_core_2_48_march_2026(acf, alice_and_remote_bob, repla
# an accepted chat alone is not enough, a message must have flowed.
alice_chat = alice_contact_bob.create_chat()
alice.set_config("keyupdate_debounce", "1")
old_addr = alice.get_config("configured_addr")
alice_chat.send_text("hi")
assert remote_eval("bob.wait_for_incoming_msg().get_snapshot().text") == "hi"
before = bob_sees()
@@ -113,8 +112,9 @@ def test_keyupdate_against_core_2_48_march_2026(acf, alice_and_remote_bob, repla
# and signature timestamps have one-second resolution:
# without waiting, the re-signed key can tie with the copy Bob holds, keeping his.
time.sleep(2)
old_addr = alice.list_transports()[0]["addr"]
alice.add_transport_from_qr(acf.get_account_qr())
(new_addr,) = [t["addr"] for t in alice.list_transports() if t["addr"] != old_addr]
new_addr = alice.list_transports()[1]["addr"]
if replace_relay:
alice.delete_transport(old_addr)
alice.bring_online()
@@ -45,10 +45,8 @@ def test_add_second_address(acf) -> None:
second_addr = account.list_transports()[1]["addr"]
third_addr = account.list_transports()[2]["addr"]
assert account.get_config("configured_addr") == first_addr
account.delete_transport(first_addr)
assert len(account.list_transports()) == 2
assert account.get_config("configured_addr") != first_addr
account.delete_transport(second_addr)
assert len(account.list_transports()) == 1
@@ -187,9 +185,6 @@ def test_transport_synchronization(acf, log) -> None:
wait_for_io_started(ac1)
[transport1, transport3] = ac1.list_transports()
log.section("ac1 changes the sending transport")
ac1.set_config("configured_addr", transport3["addr"])
log.section("ac1 removes the first transport")
ac1.delete_transport(transport1["addr"])
@@ -197,7 +192,6 @@ def test_transport_synchronization(acf, log) -> None:
wait_for_io_started(ac1_clone)
[transport3] = ac1_clone.list_transports()
assert transport3["addr"] == addr3
assert ac1_clone.get_config("configured_addr") == addr3
ac2_chat = ac2.create_chat(ac1)
ac2_chat.send_text("Hello!")
@@ -221,10 +215,6 @@ def test_transport_sync_new_as_primary(acf, log) -> None:
[transport1, transport2] = ac1_transports
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
assert len(ac1_clone.list_transports()) == 2
assert ac1_clone.get_config("configured_addr") == transport1["addr"]
log.section("ac1 changes the primary transport")
ac1.set_config("configured_addr", transport2["addr"])
log.section("ac1_clone receives a message via the new transport")
ac1_chat = ac1.create_chat(bob)
@@ -236,22 +226,6 @@ def test_transport_sync_new_as_primary(acf, log) -> None:
assert ac1_clone.wait_for_incoming_msg().get_snapshot().text == "hello back"
def test_recognize_self_address(acf) -> None:
alice, bob = acf.get_online_accounts(2)
bob_chat = bob.create_chat(alice)
qr = acf.get_account_qr()
alice.add_transport_from_qr(qr)
new_alice_addr = alice.list_transports()[1]["addr"]
alice.set_config("configured_addr", new_alice_addr)
bob_chat.send_text("Hello!")
msg = alice.wait_for_incoming_msg().get_snapshot()
assert msg.chat == alice.create_chat(bob)
def test_transport_limit(acf) -> None:
"""Test transports limit."""
account = acf.get_online_account()
@@ -294,7 +268,6 @@ def test_message_info_imap_urls(acf) -> None:
# Alice switches to another transport and removes the rest of the transports.
new_alice_addr = alice.list_transports()[1]["addr"]
alice.set_config("configured_addr", new_alice_addr)
removed_addrs = []
for transport in alice.list_transports():
if transport["addr"] != new_alice_addr:
@@ -326,7 +299,6 @@ def test_remove_primary_transport(acf, log) -> None:
log.section("Alice removes the primary relay")
alice.delete_transport(transport1["addr"])
assert alice.get_config("configured_addr") == transport2["addr"]
alice.stop_io()
alice.start_io()
@@ -369,7 +341,6 @@ def test_qr_works_after_removing_primary_transport(acf, log) -> None:
def test_background_fetch_from_second_transport(acf, direct_imap, dc):
alice, alice_chat, bob_chat = alice_with_two_transports_and_bob(acf)
[transport1, transport2] = alice.list_transports()
assert alice.get_config("configured_addr") == transport1["addr"]
alice.stop_io()
bob_chat.send_text("hello")
@@ -647,9 +647,8 @@ def test_qr_scan_updates_new_relay_address(acf):
bob.wait_for_securejoin_joiner_success()
for ac in [alice, bob]:
old_addr = ac.get_config("configured_addr")
old_addr = ac.list_transports()[0]["addr"]
ac.add_transport_from_qr(acf.get_account_qr())
ac.set_config("configured_addr", ac.list_transports()[1]["addr"])
ac.delete_transport(old_addr)
bob.secure_join(alice.get_qr_code())
+3 -3
View File
@@ -100,7 +100,7 @@ def test_lowercase_address(acf) -> None:
)
assert account.is_configured()
assert addr_upper != addr
assert account.get_config("configured_addr") == addr
assert account.list_transports()[0]["addr"] == addr
param = account.get_info()["used_transport_settings"]
@@ -168,7 +168,7 @@ def test_list_transports(acf) -> None:
def test_account(acf) -> None:
alice, bob = acf.get_online_accounts(2)
bob_addr = bob.get_config("configured_addr")
bob_addr = bob.list_transports()[0]["addr"]
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!")
@@ -331,7 +331,7 @@ def test_chat(acf) -> None:
def test_contact(acf) -> None:
alice, bob = acf.get_online_accounts(2)
bob_addr = bob.get_config("configured_addr")
bob_addr = bob.list_transports()[0]["addr"]
alice_contact_bob = alice.create_contact(bob, "Bob")
assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id)
+1 -6
View File
@@ -370,12 +370,7 @@ class ACFactory:
acname = ac._logid
addr = f"{acname}@offline.org"
lib.dc_add_pseudo_transport(ac._dc_context, as_dc_charpointer(addr))
ac.update_config(
{
"configured_addr": addr,
"displayname": acname,
},
)
ac.set_config("displayname", acname)
self._preconfigure_key(ac)
self._acsetup.init_logging(ac)
assert ac.is_configured(), "Pseudo configured account should look like if it is configured"
-1
View File
@@ -51,7 +51,6 @@ class TestGroupStressTests:
lp.sec("ac2: receiving system message about contact addition")
sysmsg = ac2._evtracker.wait_next_incoming_message()
assert ac5.get_config("configured_addr") in sysmsg.text
assert sysmsg.chat.num_contacts() == 4
lp.sec("ac5: waiting for message about addition to the chat")
-10
View File
@@ -130,11 +130,6 @@ class TestOfflineContact:
contact2.unblock()
assert ac1.get_blocked_contacts() == [contact1]
def test_create_self_contact(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
contact1 = ac1.create_contact(ac1.get_config("configured_addr"))
assert contact1.id == 1
def test_get_contacts_and_delete(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
ac2 = acfactory.get_pseudo_configured_account()
@@ -223,7 +218,6 @@ class TestOfflineChat:
ac2 = acfactory.get_pseudo_configured_account()
chat = ac1.create_group_chat(name="title1")
contact = chat.add_contact(ac2)
assert contact.addr == ac2.get_config("configured_addr")
assert contact.name == ac2.get_config("displayname")
assert contact.account == ac1
chat.remove_contact(ac2)
@@ -456,7 +450,6 @@ class TestOfflineChat:
contacts = ac2.get_contacts()
assert len(contacts) == 1
contact2 = contacts[0]
assert contact2.addr == ac_contact.get_config("configured_addr")
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2 + E2EE_INFO_MSGS
@@ -552,7 +545,6 @@ class TestOfflineChat:
contacts = ac2.get_contacts()
assert len(contacts) == 1
contact2 = contacts[0]
assert contact2.addr == ac_contact.get_config("configured_addr")
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2 + E2EE_INFO_MSGS
@@ -604,7 +596,6 @@ class TestOfflineChat:
contacts = ac2.get_contacts()
assert len(contacts) == 1
contact2 = contacts[0]
assert contact2.addr == ac_contact.get_config("configured_addr")
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2 + E2EE_INFO_MSGS
@@ -621,7 +612,6 @@ class TestOfflineChat:
contacts = ac2.get_contacts()
assert len(contacts) == 1
contact2 = contacts[0]
assert contact2.addr == ac_contact.get_config("configured_addr")
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2 + E2EE_INFO_MSGS
+4 -9
View File
@@ -2893,15 +2893,10 @@ async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Ve
let (queued_msg, side_effects) = queued_msg_pair;
if needs_encryption && !is_encrypted {
let addr = context.get_config(Config::ConfiguredAddr).await?;
let text = stock_str::unencrypted_email(
context,
addr.unwrap_or_default()
.split('@')
.nth(1)
.unwrap_or_default(),
)
.await;
// FIXME: remove this message? We don't know yet which provider we are going to send the message through.
let addr = context.get_primary_self_addr().await?;
let text =
stock_str::unencrypted_email(context, addr.split('@').nth(1).unwrap_or_default()).await;
message::set_msg_failed(context, msg, &text).await?;
add_info_msg_with_cmd(
context,
+4 -62
View File
@@ -4,7 +4,7 @@ use std::env;
use std::path::Path;
use std::str::FromStr;
use anyhow::{Context as _, Result, bail, ensure};
use anyhow::{Context as _, Result, ensure};
use base64::Engine as _;
use deltachat_contact_tools::{addr_cmp, sanitize_single_line};
use serde::{Deserialize, Serialize};
@@ -197,11 +197,6 @@ pub enum Config {
#[strum(props(default = "0"))]
DeleteDeviceAfter,
/// Deprecated(2026-09).
///
/// Use ConfiguredLoginParam and list_transports() instead.
ConfiguredAddr,
/// Deprecated(2026-04).
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
///
@@ -752,34 +747,6 @@ impl Context {
.set_raw_config(key.as_ref(), value.map(|s| s.to_lowercase()).as_deref())
.await?;
}
Config::ConfiguredAddr => {
let Some(addr) = value else {
bail!("Cannot unset configured_addr");
};
self.sql
.transaction(|transaction| {
if transaction.query_row(
"SELECT COUNT(*) FROM transports WHERE addr=?",
(addr,),
|row| {
let res: i64 = row.get(0)?;
Ok(res)
},
)? == 0
{
bail!("Address does not belong to any transport.");
}
transaction.execute(
"INSERT OR REPLACE INTO config (keyname, value) VALUES ('configured_addr', ?)",
(addr,),
)?;
Ok(())
})
.await?;
self.sql.uncache_raw_config("configured_addr").await;
}
_ => {
self.sql.set_raw_config(key.as_ref(), value).await?;
}
@@ -854,18 +821,6 @@ impl Context {
/// Determine whether the specified addr maps to the/a self addr.
/// Returns `false` if no addresses are configured.
pub(crate) async fn is_self_addr(&self, addr: &str) -> Result<bool> {
// Employ the config cache to optimize for `ConfiguredAddr` passed.
if !addr.is_empty()
&& addr_cmp(
addr,
&self
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default(),
)
{
return Ok(true);
}
Ok(self
.get_self_addrs()
.await?
@@ -873,20 +828,6 @@ impl Context {
.any(|a| addr_cmp(addr, a)))
}
/// Sets `primary_new` as the address used for sending.
///
/// This should only be used by test code and during configure.
#[cfg(test)] // AEAP is disabled, but there are still tests for it
pub(crate) async fn set_primary_self_addr(&self, primary_new: &str) -> Result<()> {
self.quota.write().await.clear();
self.sql
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(primary_new))
.await?;
self.emit_event(EventType::ConnectivityChanged);
Ok(())
}
/// Returns all self addresses, newest first.
pub(crate) async fn get_self_addrs(&self) -> Result<Vec<String>> {
let query_only = true;
@@ -895,10 +836,11 @@ impl Context {
.await
}
/// Returns the address of the transport used for sending.
/// Returns address of some transport.
/// Returns an error if no self addr is configured.
pub async fn get_primary_self_addr(&self) -> Result<String> {
self.get_config(Config::ConfiguredAddr)
self.sql
.query_get_value("SELECT addr FROM transports ORDER BY id", ())
.await?
.context("No self addr configured")
}
+4 -11
View File
@@ -37,8 +37,8 @@ use crate::sync::Sync::Nosync;
use crate::tools::time;
use crate::transport::{
ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
ConnectionCandidate, delete_transport_row, maybe_update_sending_transport,
purge_transport_caches, send_sync_transports, transport_addrs,
ConnectionCandidate, delete_transport_row, purge_transport_caches, send_sync_transports,
transport_addrs,
};
use crate::{EventType, autorelay, stock_str};
@@ -275,7 +275,7 @@ impl Context {
/// another one is chosen automatically.
pub async fn delete_transport(&self, addr: &str) -> Result<()> {
let now = time();
let (removed_transport_id, reelected) = self
let removed_transport_id = self
.sql
.transaction(|transaction| {
if transport_addrs(transaction)?.len() <= 1 {
@@ -294,14 +294,9 @@ impl Context {
let remove_timestamp = std::cmp::max(now, add_timestamp);
let transport_id = delete_transport_row(transaction, addr, remove_timestamp)?
.context("Transport disappeared")?;
let reelected = maybe_update_sending_transport(transaction)?;
Ok((transport_id, reelected))
Ok(transport_id)
})
.await?;
if let Some(new_addr) = reelected {
info!(self, "Using transport {new_addr:?} for sending now.");
self.sql.uncache_raw_config("configured_addr").await;
}
send_sync_transports(self).await?;
purge_transport_caches(self, removed_transport_id).await;
// Restarting all IO also stops the removed transport's IMAP loop.
@@ -799,8 +794,6 @@ mod tests {
let t = &tcm.unconfigured().await;
add_pseudo_transport(t, "primary@example.org").await?;
t.set_config(Config::ConfiguredAddr, Some("primary@example.org"))
.await?;
assert_eq!(t.count_transports().await?, 1);
t.check_relay_limit().await?;
+10 -34
View File
@@ -718,9 +718,9 @@ impl Contact {
.await?
.unwrap_or_default();
contact.addr = context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default();
.get_primary_self_addr()
.await
.context("Cannot get address for self-contact")?;
if let Some(self_fp) = self_fingerprint_opt(context).await? {
contact.fingerprint = Some(self_fp.to_string());
}
@@ -1421,10 +1421,6 @@ WHERE addr=?
);
let contact = Contact::get_by_id(context, contact_id).await?;
let addr = context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default();
let Some(fingerprint_other) = contact.fingerprint() else {
return Ok(stock_str::encr_none(context));
@@ -1444,32 +1440,12 @@ WHERE addr=?
.await?
.dc_fingerprint()
.human_readable();
if addr < contact.addr {
cat_fingerprint(
&mut ret,
&stock_str::self_msg(context),
&addr,
&fingerprint_self,
);
cat_fingerprint(
&mut ret,
contact.get_display_name(),
&contact.addr,
&fingerprint_other,
);
if fingerprint_self < fingerprint_other {
cat_fingerprint(&mut ret, &stock_str::self_msg(context), &fingerprint_self);
cat_fingerprint(&mut ret, contact.get_display_name(), &fingerprint_other);
} else {
cat_fingerprint(
&mut ret,
contact.get_display_name(),
&contact.addr,
&fingerprint_other,
);
cat_fingerprint(
&mut ret,
&stock_str::self_msg(context),
&addr,
&fingerprint_self,
);
cat_fingerprint(&mut ret, contact.get_display_name(), &fingerprint_other);
cat_fingerprint(&mut ret, &stock_str::self_msg(context), &fingerprint_self);
}
if let Some(public_key) = contact.public_key(context).await?
@@ -1968,8 +1944,8 @@ pub(crate) async fn update_last_seen(
Ok(())
}
fn cat_fingerprint(ret: &mut String, name: &str, addr: &str, fingerprint: &str) {
*ret += &format!("\n\n{name} ({addr}):\n{fingerprint}");
fn cat_fingerprint(ret: &mut String, name: &str, fingerprint: &str) {
*ret += &format!("\n\n{name}:\n{fingerprint}");
}
fn split_address_book(book: &str) -> Vec<(&str, &str)> {
+6 -6
View File
@@ -887,11 +887,11 @@ async fn test_contact_get_encrinfo() -> Result<()> {
"Messages are end-to-end encrypted.
Fingerprints:
Me (alice@example.org):
Me:
2E6F A2CB 23B5 32D7 2863
4B58 64B0 8F61 A9ED 9443
bob@example.net (bob@example.net):
bob@example.net:
CCCB 5AA9 F6E1 141C 9431
65F1 DB18 B18C BCF7 0487
@@ -908,11 +908,11 @@ bob@example.net"
"No encryption.
Fingerprints:
Me (alice@example.org):
Me:
2E6F A2CB 23B5 32D7 2863
4B58 64B0 8F61 A9ED 9443
bob@example.net (bob@example.net):
bob@example.net:
CCCB 5AA9 F6E1 141C 9431
65F1 DB18 B18C BCF7 0487"
);
@@ -1252,7 +1252,7 @@ async fn test_make_n_import_vcard() -> Result<()> {
tokio::fs::write(&avatar_path, avatar_bytes).await?;
bob.set_config(Config::Selfavatar, Some(avatar_path.to_str().unwrap()))
.await?;
let bob_addr = bob.get_config(Config::ConfiguredAddr).await?.unwrap();
let bob_addr = bob.get_primary_self_addr().await?;
let bob_biography = bob.get_config(Config::Selfstatus).await?.unwrap();
let chat = bob.create_chat(alice).await;
let sent_msg = bob.send_text(chat.id, "moin").await;
@@ -1348,7 +1348,7 @@ async fn test_make_n_import_vcard() -> Result<()> {
async fn test_import_vcard_key_change() -> Result<()> {
let alice = &TestContext::new_alice().await;
let bob = &TestContext::new_bob().await;
let bob_addr = &bob.get_config(Config::ConfiguredAddr).await?.unwrap();
let bob_addr = &bob.get_primary_self_addr().await?;
bob.set_config(Config::Displayname, Some("Bob")).await?;
let vcard = make_vcard(bob, &[ContactId::SELF]).await?;
alice.evtracker.clear_events();
-5
View File
@@ -961,10 +961,6 @@ mod tests {
.await;
assert!(context2.is_configured().await?);
assert_eq!(
context2.get_config(Config::ConfiguredAddr).await?,
Some("alice@example.org".to_string())
);
Ok(())
}
@@ -1074,7 +1070,6 @@ mod tests {
.await;
assert!(!context2.is_configured().await?);
assert_eq!(context2.get_config(Config::ConfiguredAddr).await?, None);
Ok(())
}
-10
View File
@@ -660,7 +660,6 @@ mod tests {
use std::time::Duration;
use super::*;
use crate::config::Config;
use crate::test_utils::{TestContext, TestContextManager, alice_keypair};
use crate::tools::SystemTime;
use crate::transport::add_pseudo_transport;
@@ -814,9 +813,6 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
async fn test_load_self_generate_public() {
let t = TestContext::new().await;
add_pseudo_transport(&t, "alice@example.org").await.unwrap();
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
.await
.unwrap();
let key = load_self_public_key(&t).await;
assert!(key.is_ok());
}
@@ -825,9 +821,6 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
async fn test_load_self_generate_secret() {
let t = TestContext::new().await;
add_pseudo_transport(&t, "alice@example.org").await.unwrap();
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
.await
.unwrap();
let key = load_self_secret_key(&t).await;
assert!(key.is_ok());
}
@@ -838,9 +831,6 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
let t = TestContext::new().await;
add_pseudo_transport(&t, "alice@example.org").await.unwrap();
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
.await
.unwrap();
let thr0 = {
let ctx = t.clone();
thread::spawn(move || {
-4
View File
@@ -260,10 +260,6 @@ async fn test_keyupdate_on_replacement() -> Result<()> {
alice.add_transport("alice@relay.example.net").await;
SystemTime::shift(Duration::from_secs(2));
alice.delete_transport("alice@example.org").await?;
assert_eq!(
alice.get_config(Config::ConfiguredAddr).await?.as_deref(),
Some("alice@relay.example.net")
);
maybe_send_keyupdate_message(alice).await?;
let keyupdate = alice.pop_sent_msg().await;
+7 -11
View File
@@ -433,9 +433,9 @@ pub(crate) fn render_queued_mail(
#[cfg(test)]
pub(crate) async fn render_queued_mail_with_context(
queued_mail: QueuedMail,
context: &Context,
context: &crate::test_utils::TestContext,
) -> Result<RenderedEmail> {
let from_addr = context.get_primary_self_addr().await?;
let from_addr = context.sending_addr().await;
let public_key = crate::key::load_self_public_key(context).await?;
let secret_key = crate::key::load_self_secret_key(context).await?;
@@ -999,14 +999,10 @@ impl MimeFactory {
true => context.get_config(Config::Displayname).await?,
false => None,
};
let self_name = &match self_name {
Some(name) => name,
None => context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default(),
};
Ok(stock_str::subject_for_new_contact(context, self_name))
Ok(stock_str::subject_for_new_contact(
context,
self_name.as_deref().unwrap_or("..."),
))
}
pub fn recipients(&self) -> Vec<String> {
@@ -1228,7 +1224,7 @@ impl MimeFactory {
///
/// Used only for tests.
#[cfg(test)]
pub async fn render(self, context: &Context) -> Result<RenderedEmail> {
pub async fn render(self, context: &crate::test_utils::TestContext) -> Result<RenderedEmail> {
// Does not matter, we are not going to return the QueuedMail.
let bcc_self = false;
let (queued_mail, _side_effects) =
+9 -12
View File
@@ -185,7 +185,7 @@ async fn test_subject_outgoing() {
let t = TestContext::new_alice().await;
t.allow_unencrypted().await.unwrap();
assert_eq!(first_subject_str(t).await, "Message from alice@example.org");
assert_eq!(first_subject_str(t).await, "Message from ...");
let t = TestContext::new_alice().await;
t.allow_unencrypted().await.unwrap();
@@ -732,9 +732,9 @@ async fn test_remove_member_bcc() -> Result<()> {
let charlie = &tcm.charlie().await;
alice.allow_unencrypted().await?;
let alice_addr = alice.get_config(Config::ConfiguredAddr).await?.unwrap();
let bob_addr = bob.get_config(Config::ConfiguredAddr).await?.unwrap();
let charlie_addr = charlie.get_config(Config::ConfiguredAddr).await?.unwrap();
let alice_addr = alice.get_primary_self_addr().await?;
let bob_addr = bob.get_primary_self_addr().await?;
let charlie_addr = charlie.get_primary_self_addr().await?;
let bob_id = alice.add_or_lookup_address_contact_id(bob).await;
let charlie_id = alice.add_or_lookup_address_contact_id(charlie).await;
@@ -897,7 +897,7 @@ async fn test_new_member_is_first_recipient() -> Result<()> {
assert!(
sent_msg
.recipients
.starts_with(&charlie.get_config(Config::ConfiguredAddr).await?.unwrap())
.starts_with(&charlie.get_primary_self_addr().await?)
);
remove_contact_from_chat(alice, group, bob_id).await?;
@@ -905,11 +905,8 @@ async fn test_new_member_is_first_recipient() -> Result<()> {
SystemTime::shift(Duration::from_secs(60));
add_contact_to_chat(alice, group, bob_id).await?;
let sent_msg = alice.pop_sent_msg().await;
assert!(
sent_msg
.recipients
.starts_with(&bob.get_config(Config::ConfiguredAddr).await?.unwrap())
);
let bob_addr = bob.get_primary_self_addr().await?;
assert!(sent_msg.recipients.starts_with(&bob_addr));
Ok(())
}
@@ -1117,7 +1114,7 @@ Autocrypt: addr=alice@example.org; prefer-encrypt=mutual;
Content-Type: text/plain; charset="utf-8"
Date: DATE
To: <bob@example.net>
Subject: Message from alice@example.org
Subject: Message from ...
References: <MESSAGE_ID@localhost>
Chat-Version: 1.0
Content-Transfer-Encoding: 7bit
@@ -1169,7 +1166,7 @@ Content-Type: multipart/mixed;
boundary="BOUNDARY"
Date: DATE
To: <bob@example.net>
Subject: Message from alice@example.org
Subject: Message from ...
References: <MESSAGE_ID@localhost>
Chat-Version: 1.0
@@ -131,7 +131,7 @@ async fn test_broadcast_security_attacker_signature() -> Result<()> {
let secret = load_broadcast_secret(alice, alice_chat_id).await?.unwrap();
let charlie_addr = charlie.get_config(Config::ConfiguredAddr).await?.unwrap();
let charlie_addr = charlie.get_primary_self_addr().await?;
test_shared_secret_decryption_ext(
bob,
@@ -183,7 +183,7 @@ async fn test_broadcast_security_happy_path() -> Result<()> {
let secret = load_broadcast_secret(alice, alice_chat_id).await?.unwrap();
let alice_addr = alice.get_config(Config::ConfiguredAddr).await?.unwrap();
let alice_addr = alice.get_primary_self_addr().await?;
test_shared_secret_decryption_ext(bob, &alice_addr, &secret, Some(alice), None).await
}
@@ -202,7 +202,7 @@ async fn test_qr_code_security() -> Result<()> {
// Start a securejoin process, but don't finish it:
join_securejoin(bob, &qr).await?;
let charlie_addr = charlie.get_config(Config::ConfiguredAddr).await?.unwrap();
let charlie_addr = charlie.get_primary_self_addr().await?;
let alice_fp = self_fingerprint(alice).await?;
let secret_for_encryption = format!("securejoin/{alice_fp}/{authcode}");
-4
View File
@@ -4,7 +4,6 @@ use tokio::sync::OnceCell;
use super::*;
use crate::transport::add_pseudo_transport;
use crate::{
config::Config,
decrypt,
key::{load_self_public_key, self_fingerprint, store_self_keypair},
mimefactory::{part_to_bytes, wrap_encrypted_part},
@@ -23,9 +22,6 @@ async fn decrypt_bytes(
add_pseudo_transport(t, "alice@example.org")
.await
.expect("Failed to add pseudo transport");
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
.await
.expect("Failed to configure address");
for secret in auth_tokens_for_decryption {
token::save(t, token::Namespace::Auth, None, secret, 0).await?;
-8
View File
@@ -156,14 +156,6 @@ mod tests {
};
assert!(!t.quota_needs_update(0, TIMEOUT).await);
t.evtracker.clear_events();
t.set_primary_self_addr("new@addr").await?;
assert!(t.quota.read().await.is_empty());
t.evtracker
.get_matching(|evt| matches!(evt, EventType::ConnectivityChanged))
.await;
assert!(t.quota_needs_update(0, TIMEOUT).await);
Ok(())
}
}
+4 -7
View File
@@ -3853,7 +3853,7 @@ async fn test_unsigned_chat_group_hdr() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let bob_addr = bob.get_config(Config::ConfiguredAddr).await?.unwrap();
let bob_addr = bob.get_primary_self_addr().await?;
let bob_id = alice.add_or_lookup_contact_id(bob).await;
let alice_chat_id = create_group(alice, "foos").await?;
add_contact_to_chat(alice, alice_chat_id, bob_id).await?;
@@ -4541,7 +4541,7 @@ async fn test_outgoing_msg_forgery() -> Result<()> {
let mut tcm = TestContextManager::new();
let export_dir = tempfile::tempdir().unwrap();
let alice = &tcm.alice().await;
let alice_addr = &alice.get_config(Config::ConfiguredAddr).await?.unwrap();
let alice_addr = &alice.get_primary_self_addr().await?;
imex(alice, ImexMode::ExportSelfKeys, export_dir.path(), None).await?;
// We need Bob only to encrypt the forged message to Alice's key, actually Bob doesn't
// participate in the scenario.
@@ -5638,7 +5638,7 @@ async fn test_bcc_not_a_group() -> Result<()> {
async fn test_lookup_key_contact_by_address_self() -> Result<()> {
let mut tcm = TestContextManager::new();
let t = &tcm.alice().await;
let addr = &t.get_config(Config::ConfiguredAddr).await?.unwrap();
let addr = &t.get_primary_self_addr().await?;
assert_eq!(
lookup_key_contact_by_address(t, addr, None).await?,
Some(ContactId::SELF)
@@ -5690,10 +5690,7 @@ async fn test_outgoing_determined_by_signature() -> Result<()> {
let alice_dev2 = &tcm.unconfigured().await;
alice_dev2.configure_addr(different_from).await;
key::store_self_keypair(alice_dev2, &alice_keypair()).await?;
assert_ne!(
alice.get_config(Config::ConfiguredAddr).await?.unwrap(),
different_from
);
assert_ne!(alice.get_primary_self_addr().await?, different_from);
// Send message from alice_dev2 and check alice sees it as outgoing
let chat_id = alice_dev2.create_chat_id(bob).await;
+2 -6
View File
@@ -50,11 +50,7 @@ async fn test_setup_contact_ext(case: SetupContactCase) -> (TestContext, TestCon
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let alice_addr = &alice
.get_config(Config::ConfiguredAddr)
.await
.unwrap()
.unwrap();
let alice_addr = alice.get_primary_self_addr().await.unwrap();
if case == SetupContactCase::AliceHasName {
alice
.set_config(Config::Displayname, Some("Alice"))
@@ -118,7 +114,7 @@ async fn test_setup_contact_ext(case: SetupContactCase) -> (TestContext, TestCon
let contact_alice_id = bob.add_or_lookup_contact_no_key(&alice).await.id;
let sent = bob.pop_sent_msg().await;
assert!(!sent.payload.contains("Bob Examplenet"));
assert_eq!(sent.recipient(), EmailAddress::new(alice_addr).unwrap());
assert_eq!(sent.recipients, *alice_addr);
let msg = alice.parse_msg(&sent).await;
assert!(msg.signature.is_none());
assert_eq!(
+5 -10
View File
@@ -13,7 +13,6 @@ use rusqlite::OptionalExtension as _;
use tokio::task;
use crate::chat::{ChatId, add_info_msg_with_cmd};
use crate::config::Config;
use crate::contact::{Contact, ContactId};
use crate::context::Context;
use crate::events::EventType;
@@ -587,15 +586,11 @@ pub(crate) async fn send_msg_to_smtp(
.await?;
if let Some((chat_id, timestamp_sort)) = res {
let addr = context.get_config(Config::ConfiguredAddr).await?;
let text = unencrypted_email(
context,
addr.unwrap_or_default()
.split('@')
.nth(1)
.unwrap_or_default(),
)
.await;
// FIXME info message
let addr = context.get_primary_self_addr().await?;
let text =
unencrypted_email(context, addr.split('@').nth(1).unwrap_or_default())
.await;
add_info_msg_with_cmd(
context,
chat_id,
File diff suppressed because one or more lines are too long
+29 -31
View File
@@ -193,21 +193,11 @@ impl TestContextManager {
to.recv_msg(&sent).await
}
pub async fn change_addr(&self, test_context: &TestContext, new_addr: &str) {
self.section(&format!(
"{} changes her self address and reconfigures",
test_context.name()
));
pub async fn change_addr(&self, test_context: &mut TestContext, new_addr: &str) {
test_context.add_transport(new_addr).await;
test_context.set_primary_self_addr(new_addr).await.unwrap();
test_context.set_sending_addr(new_addr);
// ensure_secret_key_exists() is called during configure
key::ensure_secret_key_exists(test_context).await.unwrap();
assert_eq!(
test_context.get_primary_self_addr().await.unwrap(),
new_addr
);
}
/// Executes SecureJoin protocol between `scanner` and `scanned`.
@@ -252,10 +242,9 @@ impl TestContextManager {
qr: &str,
) -> ChatId {
assert!(joiner.pop_sent_msg_opt().await.is_none());
let inviter_addr = inviters[0].get_primary_self_addr().await.unwrap();
let inviter_addr = inviters[0].sending_addr().await;
for inviter in inviters {
assert!(inviter.pop_sent_msg_opt().await.is_none());
assert_eq!(inviter.get_primary_self_addr().await.unwrap(), inviter_addr);
}
let chat_id = join_securejoin(&joiner.ctx, qr).await.unwrap();
@@ -435,6 +424,9 @@ pub struct TestContext {
pub evtracker: EventTracker,
log_sink: LogSink,
/// From address to be used when emulating sending.
from: Option<String>,
}
impl TestContext {
@@ -530,6 +522,7 @@ impl TestContext {
dir,
evtracker: EventTracker::new(evtracker_receiver),
log_sink,
from: None,
}
}
@@ -560,10 +553,6 @@ impl TestContext {
add_pseudo_transport(&self.ctx, addr)
.await
.expect("Failed to add pseudo transport");
self.ctx
.set_config(Config::ConfiguredAddr, Some(addr))
.await
.expect("Failed to configure address");
if let Some(name) = addr.split('@').next() {
self.set_name(name);
@@ -586,6 +575,21 @@ impl TestContext {
self.self_public_key.lock().await.take();
}
pub fn set_sending_addr(&mut self, addr: &str) {
// TODO: check if the address is configured
self.from = Some(addr.to_string());
}
pub async fn sending_addr(&self) -> String {
if let Some(ref from) = self.from {
from.clone()
} else {
self.get_primary_self_addr()
.await
.expect("Cannot get From address")
}
}
/// Retrieves a sent message from the jobs table.
///
/// This retrieves and removes a message which has been scheduled to send from the jobs
@@ -632,10 +636,7 @@ ORDER BY id"
.await
.expect("Failed to load queued mail");
if queued_mail.bcc_self {
let from = self
.get_primary_self_addr()
.await
.expect("Cannot get From address");
let from = self.sending_addr().await;
smtp::add_self_recipients(
&self.ctx,
&mut queued_mail.recipients,
@@ -734,10 +735,7 @@ ORDER BY id"
.await
.expect("Failed to load queued mail");
if queued_mail.bcc_self {
let from = self
.get_primary_self_addr()
.await
.expect("Cannot get self address");
let from = self.sending_addr().await;
smtp::add_self_recipients(
&self.ctx,
&mut queued_mail.recipients,
@@ -897,7 +895,7 @@ ORDER BY id"
/// Returns the [`ContactId`] for the other [`TestContext`], creating a contact if necessary.
pub async fn add_or_lookup_address_contact_id(&self, other: &TestContext) -> ContactId {
let primary_self_addr = other.ctx.get_primary_self_addr().await.unwrap();
let primary_self_addr = other.sending_addr().await;
let addr = ContactAddress::new(&primary_self_addr).unwrap();
// MailinglistAddress is the lowest allowed origin, we'd prefer to not modify the
// origin when creating this contact.
@@ -952,7 +950,7 @@ ORDER BY id"
/// If the contact does not exist yet, a new contact will be created
/// with the correct fingerprint, but without the public key.
pub async fn add_or_lookup_contact_id_no_key(&self, other: &TestContext) -> ContactId {
let primary_self_addr = other.ctx.get_primary_self_addr().await.unwrap();
let primary_self_addr = other.sending_addr().await;
let addr = ContactAddress::new(&primary_self_addr).unwrap();
let fingerprint = self_fingerprint(other).await.unwrap();
@@ -1242,13 +1240,14 @@ ORDER BY id"
}
pub async fn encrypt_raw_message(
context: &Context,
context: &TestContext,
receivers: &[&TestContext],
payload: &[u8],
) -> Result<String> {
let public_key = key::load_self_public_key(context).await?;
let from = context.sending_addr().await;
let aheader = Aheader {
addr: context.get_primary_self_addr().await?,
addr: from.clone(),
public_key: public_key.clone(),
prefer_encrypt: EncryptPreference::Mutual,
};
@@ -1259,7 +1258,6 @@ pub async fn encrypt_raw_message(
encryption_keyring.push(key::load_self_public_key(receiver).await?);
}
let from = context.get_primary_self_addr().await?;
let compress = false;
let mut cleartext = format!("Autocrypt: {aheader}").into_bytes();
+6 -8
View File
@@ -20,13 +20,14 @@ use crate::test_utils::TestContextManager;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_change_primary_self_addr() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let mut alice = tcm.alice().await;
let bob = tcm.bob().await;
tcm.send_recv_accept(&alice, &bob, "Hi").await;
let bob_alice_chat = bob.create_chat(&alice).await;
tcm.change_addr(&alice, "alice@someotherdomain.xyz").await;
tcm.change_addr(&mut alice, "alice@someotherdomain.xyz")
.await;
tcm.section("Bob sends a message to Alice, encrypting to her previous key");
let sent = bob.send_text(bob_alice_chat.id, "hi back").await;
@@ -63,7 +64,7 @@ async fn check_aeap_transition(chat_for_transition: ChatForTransition) {
const ALICE_NEW_ADDR: &str = "alice2@example.net";
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let alice = &mut tcm.alice().await;
let bob = &tcm.bob().await;
tcm.send_recv_accept(alice, bob, "Hi").await;
@@ -105,10 +106,7 @@ async fn check_aeap_transition(chat_for_transition: ChatForTransition) {
check_that_transition_worked(bob, &groups, alice_contact, ALICE_NEW_ADDR).await;
tcm.section("Test switching back");
alice
.set_primary_self_addr("alice@example.org")
.await
.unwrap();
alice.set_sending_addr("alice@example.org");
let sent = alice
.send_text(chat_to_send, "Hello from my old addr!")
.await;
@@ -205,7 +203,7 @@ async fn test_aeap_replay_attack() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_write_to_alice_after_aeap() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let alice = &mut tcm.alice().await;
let bob = &tcm.bob().await;
let alice_grp_id = chat::create_group(alice, "Group").await?;
+6 -46
View File
@@ -276,14 +276,15 @@ impl ConfiguredLoginParam {
.await
}
/// Loads legacy configured param. Only used for tests and the migration.
/// Loads legacy configured param. Only used for tests and migration 131.
pub(crate) async fn load_legacy(context: &Context) -> Result<Option<Self>> {
if !context.get_config_bool(Config::Configured).await? {
return Ok(None);
}
let addr = context
.get_config(Config::ConfiguredAddr)
.sql
.get_raw_config("configured_addr")
.await?
.unwrap_or_default()
.trim()
@@ -480,8 +481,7 @@ pub(crate) async fn save_transport(
);
let addr = addr_normalize(&configured.addr);
let configured_addr = context.get_config(Config::ConfiguredAddr).await?;
let mut modified = context
let modified = context
.sql
.execute(
"INSERT INTO transports (addr, entered_param, configured_param, add_timestamp)
@@ -503,14 +503,6 @@ pub(crate) async fn save_transport(
.await?
> 0;
if configured_addr.is_none() {
// If there is no transport yet, use the new transport for sending
context
.sql
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr))
.await?;
modified = true;
}
Ok(modified)
}
@@ -625,7 +617,7 @@ pub(crate) async fn sync_transports(
)
.collect();
let (deleted_ids, reelected) = context
let deleted_ids = context
.sql
.transaction(|transaction| {
let mut deleted_ids = Vec::new();
@@ -638,8 +630,7 @@ pub(crate) async fn sync_transports(
}
modified |= !deleted_ids.is_empty();
let reelected = maybe_update_sending_transport(transaction)?;
Ok((deleted_ids, reelected))
Ok(deleted_ids)
})
.await?;
@@ -647,12 +638,6 @@ pub(crate) async fn sync_transports(
purge_transport_caches(context, *transport_id).await;
}
if let Some(new_addr) = reelected {
info!(context, "Re-elected sending transport {new_addr:?}.");
context.sql.uncache_raw_config("configured_addr").await;
modified = true;
}
if modified {
context.self_public_key.lock().await.take();
context
@@ -711,31 +696,6 @@ pub(crate) async fn purge_transport_caches(context: &Context, transport_id: u32)
context.metadata.write().await.remove(&transport_id);
}
/// Elects another transport for sending if the current one vanished.
/// Any remaining transport works and selection is anyway moving
/// to the authority of the SMTP loop, see <https://github.com/chatmail/core/pull/8619>
pub(crate) fn maybe_update_sending_transport(
transaction: &mut rusqlite::Transaction,
) -> Result<Option<String>> {
let configured_addr: String = transaction.query_row(
"SELECT value FROM config WHERE keyname='configured_addr'",
(),
|row| row.get(0),
)?;
let addrs = transport_addrs(transaction)?;
if addrs.contains(&configured_addr) {
return Ok(None);
}
let Some(new_addr) = addrs.into_iter().next() else {
return Ok(None);
};
transaction.execute(
"UPDATE config SET value=? WHERE keyname='configured_addr'",
(&new_addr,),
)?;
Ok(Some(new_addr))
}
/// Adds transport entry to the `transports` table with empty configuration.
pub async fn add_pseudo_transport(context: &Context, addr: &str) -> Result<()> {
context.sql
-91
View File
@@ -197,10 +197,6 @@ async fn test_delete_transport() -> Result<()> {
.evtracker
.get_matching(|e| matches!(e, EventType::TransportsModified))
.await;
assert_eq!(
alice.get_config(Config::ConfiguredAddr).await?.as_deref(),
Some("alice@otherprovider.com")
);
sync_and_check_recipients(alice, alice2, "alice@otherprovider.com").await;
check_addrs(
@@ -226,50 +222,6 @@ async fn test_delete_transport() -> Result<()> {
Ok(())
}
/// Tests that selecting sending transport by setting "configured_addr" does not
/// send the sync message, is not synchronized between devices even if sync message is forced,
/// and does not bump sending transport `add_timestamp`.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_no_configured_addr_synchronization() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let alice2 = &tcm.alice().await;
for a in [alice, alice2] {
a.set_config_bool(Config::SyncMsgs, true).await?;
a.set_config_bool(Config::BccSelf, true).await?;
}
let addr = "alice@otherprovider.com";
add_dummy_transport(alice, addr).await?;
send_sync_transports(alice).await?;
sync_and_check_recipients(alice, alice2, &format!("{addr} alice@example.org")).await;
// Selects `addr` on `alice` as the sending transport
// and syncs the transport update to `alice2`,
// whose own sending transport must stay unchanged.
let old_timestamp = add_timestamp(alice2, addr).await;
let alice2_primary = alice2.get_config(Config::ConfiguredAddr).await?;
alice.set_config(Config::ConfiguredAddr, Some(addr)).await?;
assert_eq!(add_timestamp(alice, addr).await, old_timestamp);
send_sync_transports(alice).await?;
alice.send_sync_msg().await?.unwrap();
let sync_msg = alice.pop_sent_msg().await;
assert_eq!(sync_msg.recipients, format!("alice@example.org {addr}"));
// The sync message comes from the new sending address,
// which must not make `alice2` adopt it as its own sending address.
assert!(sync_msg.payload.contains(&format!("From: <{addr}>")));
alice2.recv_msg_trash(&sync_msg).await;
// add_timestamp must not change.
assert_eq!(add_timestamp(alice2, addr).await, old_timestamp);
assert_eq!(
alice2.get_config(Config::ConfiguredAddr).await?,
alice2_primary
);
Ok(())
}
/// Tests that `sync_transports()` requests an IO restart
/// if and only if it modified anything.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -288,14 +240,6 @@ async fn test_sync_transports_requests_io_restart() -> Result<()> {
Ok(())
}
async fn add_timestamp(t: &TestContext, addr: &str) -> i64 {
t.sql
.query_get_value("SELECT add_timestamp FROM transports WHERE addr=?", (addr,))
.await
.unwrap()
.unwrap()
}
/// Tests that removing the last transport keeps it.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_removing_last_transport() -> Result<()> {
@@ -308,10 +252,6 @@ async fn test_removing_last_transport() -> Result<()> {
};
sync_transports(alice, &[], std::slice::from_ref(&removed)).await?;
assert_eq!(alice.count_transports().await?, 1);
assert_eq!(
alice.get_config(Config::ConfiguredAddr).await?.as_deref(),
Some("alice@otherprovider.com")
);
let removed = RemovedTransportData {
addr: "alice@otherprovider.com".to_string(),
@@ -319,37 +259,6 @@ async fn test_removing_last_transport() -> Result<()> {
};
sync_transports(alice, &[], std::slice::from_ref(&removed)).await?;
assert_eq!(alice.count_transports().await?, 1);
assert_eq!(
alice.get_config(Config::ConfiguredAddr).await?.as_deref(),
Some("alice@otherprovider.com")
);
Ok(())
}
/// Tests which transport is elected for sending.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_maybe_update_sending_transport() -> Result<()> {
let t = &TestContext::new_alice().await;
add_dummy_transport(t, "alice@one.com").await?;
assert_eq!(
t.sql.transaction(maybe_update_sending_transport).await?,
None
);
t.sql
.execute(
"DELETE FROM transports WHERE addr=?",
("alice@example.org",),
)
.await?;
assert_eq!(
t.sql
.transaction(maybe_update_sending_transport)
.await?
.as_deref(),
Some("alice@one.com")
);
Ok(())
}