diff --git a/deltachat-rpc-client/tests/test_multitransport.py b/deltachat-rpc-client/tests/test_multitransport.py
index 2c318193d..d3a3be992 100644
--- a/deltachat-rpc-client/tests/test_multitransport.py
+++ b/deltachat-rpc-client/tests/test_multitransport.py
@@ -1,5 +1,6 @@
import pytest
+from deltachat_rpc_client import EventType
from deltachat_rpc_client.rpc import JsonRpcError
@@ -156,3 +157,47 @@ def test_reconfigure_transport(acfactory) -> None:
# Reconfiguring the transport should not reset
# the settings as if when configuring the first transport.
assert account.get_config("mvbox_move") == "1"
+
+
+def test_transport_synchronization(acfactory, log) -> None:
+ """Test synchronization of transports between devices."""
+ ac1, ac2 = acfactory.get_online_accounts(2)
+ ac1_clone = ac1.clone()
+ ac1_clone.bring_online()
+
+ qr = acfactory.get_account_qr()
+
+ ac1.add_transport_from_qr(qr)
+ ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
+ assert len(ac1.list_transports()) == 2
+ assert len(ac1_clone.list_transports()) == 2
+
+ ac1_clone.add_transport_from_qr(qr)
+ ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
+ assert len(ac1.list_transports()) == 3
+ assert len(ac1_clone.list_transports()) == 3
+
+ log.section("ac1 clone removes second transport")
+ [transport1, transport2, transport3] = ac1_clone.list_transports()
+ addr3 = transport3["addr"]
+ ac1_clone.delete_transport(transport2["addr"])
+
+ ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
+ [transport1, transport3] = ac1.list_transports()
+
+ log.section("ac1 changes the primary transport")
+ ac1.set_config("configured_addr", transport3["addr"])
+
+ log.section("ac1 removes the first transport")
+ ac1.delete_transport(transport1["addr"])
+
+ ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
+ [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!")
+
+ assert ac1.wait_for_incoming_msg().get_snapshot().text == "Hello!"
+ assert ac1_clone.wait_for_incoming_msg().get_snapshot().text == "Hello!"
diff --git a/src/config.rs b/src/config.rs
index 9443dbd62..13c245718 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -819,11 +819,19 @@ impl Context {
self,
"Creating a pseudo configured account which will not be able to send or receive messages. Only meant for tests!"
);
- ConfiguredLoginParam::from_json(&format!(
- r#"{{"addr":"{addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#
- ))?
- .save_to_transports_table(self, &EnteredLoginParam::default())
- .await?;
+ self.sql
+ .execute(
+ "INSERT INTO transports (addr, entered_param, configured_param) VALUES (?, ?, ?)",
+ (
+ addr,
+ serde_json::to_string(&EnteredLoginParam::default())?,
+ format!(r#"{{"addr":"{addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#)
+ ),
+ )
+ .await?;
+ self.sql
+ .set_raw_config(Config::ConfiguredAddr.as_ref(), Some(addr))
+ .await?;
}
self.sql
.transaction(|transaction| {
diff --git a/src/configure.rs b/src/configure.rs
index 63495f009..80834e482 100644
--- a/src/configure.rs
+++ b/src/configure.rs
@@ -40,7 +40,7 @@ use crate::sync::Sync::*;
use crate::tools::time;
use crate::transport::{
ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
- ConnectionCandidate,
+ ConnectionCandidate, send_sync_transports,
};
use crate::{EventType, stock_str};
use crate::{chat, provider};
@@ -205,6 +205,7 @@ impl Context {
/// Removes the transport with the specified email address
/// (i.e. [EnteredLoginParam::addr]).
pub async fn delete_transport(&self, addr: &str) -> Result<()> {
+ let now = time();
self.sql
.transaction(|transaction| {
let primary_addr = transaction.query_row(
@@ -219,12 +220,13 @@ impl Context {
if primary_addr == addr {
bail!("Cannot delete primary transport");
}
- let transport_id = transaction.query_row(
- "DELETE FROM transports WHERE addr=? RETURNING id",
+ let (transport_id, add_timestamp) = transaction.query_row(
+ "DELETE FROM transports WHERE addr=? RETURNING id, add_timestamp",
(addr,),
|row| {
let id: u32 = row.get(0)?;
- Ok(id)
+ let add_timestamp: i64 = row.get(1)?;
+ Ok((id, add_timestamp))
},
)?;
transaction.execute("DELETE FROM imap WHERE transport_id=?", (transport_id,))?;
@@ -233,9 +235,23 @@ impl Context {
(transport_id,),
)?;
+ // Removal timestamp should not be lower than addition timestamp
+ // to be accepted by other devices when synced.
+ let remove_timestamp = std::cmp::max(now, add_timestamp);
+
+ transaction.execute(
+ "INSERT INTO removed_transports (addr, remove_timestamp)
+ VALUES (?, ?)
+ ON CONFLICT (addr)
+ DO UPDATE SET remove_timestamp = excluded.remove_timestamp",
+ (addr, remove_timestamp),
+ )?;
+
Ok(())
})
.await?;
+ send_sync_transports(self).await?;
+
Ok(())
}
@@ -552,7 +568,8 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result