Skip to content

Commit 38eac74

Browse files
committed
feat: synchronize transports via sync messages
1 parent b18a774 commit 38eac74

File tree

8 files changed

+310
-56
lines changed

8 files changed

+310
-56
lines changed

deltachat-rpc-client/tests/test_multitransport.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import pytest
22

3+
from deltachat_rpc_client import EventType
34
from deltachat_rpc_client.rpc import JsonRpcError
45

56

@@ -156,3 +157,47 @@ def test_reconfigure_transport(acfactory) -> None:
156157
# Reconfiguring the transport should not reset
157158
# the settings as if when configuring the first transport.
158159
assert account.get_config("mvbox_move") == "1"
160+
161+
162+
def test_transport_synchronization(acfactory, log) -> None:
163+
"""Test synchronization of transports between devices."""
164+
ac1, ac2 = acfactory.get_online_accounts(2)
165+
ac1_clone = ac1.clone()
166+
ac1_clone.bring_online()
167+
168+
qr = acfactory.get_account_qr()
169+
170+
ac1.add_transport_from_qr(qr)
171+
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
172+
assert len(ac1.list_transports()) == 2
173+
assert len(ac1_clone.list_transports()) == 2
174+
175+
ac1_clone.add_transport_from_qr(qr)
176+
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
177+
assert len(ac1.list_transports()) == 3
178+
assert len(ac1_clone.list_transports()) == 3
179+
180+
log.section("ac1 clone removes second transport")
181+
[transport1, transport2, transport3] = ac1_clone.list_transports()
182+
addr3 = transport3["addr"]
183+
ac1_clone.delete_transport(transport2["addr"])
184+
185+
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
186+
[transport1, transport3] = ac1.list_transports()
187+
188+
log.section("ac1 changes the primary transport")
189+
ac1.set_config("configured_addr", transport3["addr"])
190+
191+
log.section("ac1 removes the first transport")
192+
ac1.delete_transport(transport1["addr"])
193+
194+
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
195+
[transport3] = ac1_clone.list_transports()
196+
assert transport3["addr"] == addr3
197+
assert ac1_clone.get_config("configured_addr") == addr3
198+
199+
ac2_chat = ac2.create_chat(ac1)
200+
ac2_chat.send_text("Hello!")
201+
202+
assert ac1.wait_for_incoming_msg().get_snapshot().text == "Hello!"
203+
assert ac1_clone.wait_for_incoming_msg().get_snapshot().text == "Hello!"

src/config.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -819,11 +819,19 @@ impl Context {
819819
self,
820820
"Creating a pseudo configured account which will not be able to send or receive messages. Only meant for tests!"
821821
);
822-
ConfiguredLoginParam::from_json(&format!(
823-
r#"{{"addr":"{addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#
824-
))?
825-
.save_to_transports_table(self, &EnteredLoginParam::default())
826-
.await?;
822+
self.sql
823+
.execute(
824+
"INSERT INTO transports (addr, entered_param, configured_param) VALUES (?, ?, ?)",
825+
(
826+
addr,
827+
serde_json::to_string(&EnteredLoginParam::default())?,
828+
format!(r#"{{"addr":"{addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#)
829+
),
830+
)
831+
.await?;
832+
self.sql
833+
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(addr))
834+
.await?;
827835
}
828836
self.sql
829837
.transaction(|transaction| {

src/configure.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ use crate::sync::Sync::*;
4040
use crate::tools::time;
4141
use crate::transport::{
4242
ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
43-
ConnectionCandidate,
43+
ConnectionCandidate, send_sync_transports,
4444
};
4545
use crate::{EventType, stock_str};
4646
use crate::{chat, provider};
@@ -205,6 +205,7 @@ impl Context {
205205
/// Removes the transport with the specified email address
206206
/// (i.e. [EnteredLoginParam::addr]).
207207
pub async fn delete_transport(&self, addr: &str) -> Result<()> {
208+
let now = time();
208209
self.sql
209210
.transaction(|transaction| {
210211
let primary_addr = transaction.query_row(
@@ -232,10 +233,19 @@ impl Context {
232233
"DELETE FROM imap_sync WHERE transport_id=?",
233234
(transport_id,),
234235
)?;
236+
transaction.execute(
237+
"INSERT INTO removed_transports (addr, remove_timestamp)
238+
VALUES (?, ?)
239+
ON CONFLICT (addr)
240+
DO UPDATE SET remove_timestamp = excluded.remove_timestamp",
241+
(addr, now),
242+
)?;
235243

236244
Ok(())
237245
})
238246
.await?;
247+
send_sync_transports(self).await?;
248+
239249
Ok(())
240250
}
241251

@@ -552,7 +562,8 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
552562

553563
progress!(ctx, 900);
554564

555-
if !ctx.is_configured().await? {
565+
let is_configured = ctx.is_configured().await?;
566+
if !is_configured {
556567
ctx.sql.set_raw_config("mvbox_move", Some("0")).await?;
557568
ctx.sql.set_raw_config("only_fetch_mvbox", None).await?;
558569
}
@@ -563,8 +574,10 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
563574

564575
let provider = configured_param.provider;
565576
configured_param
566-
.save_to_transports_table(ctx, param)
577+
.clone()
578+
.save_to_transports_table(ctx, param, time())
567579
.await?;
580+
send_sync_transports(ctx).await?;
568581

569582
ctx.set_config_internal(Config::ConfiguredTimestamp, Some(&time().to_string()))
570583
.await?;

src/receive_imf.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -827,6 +827,11 @@ pub(crate) async fn receive_imf_inner(
827827
if let Some(ref sync_items) = mime_parser.sync_items {
828828
if from_id == ContactId::SELF {
829829
if mime_parser.was_encrypted() {
830+
// Receiving encrypted message from self updates primary transport.
831+
context
832+
.sql
833+
.set_raw_config("configured_addr", Some(&mime_parser.from.addr))
834+
.await?;
830835
context
831836
.execute_sync_items(sync_items, mime_parser.timestamp_sent)
832837
.await;

src/sql/migrations.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1439,6 +1439,21 @@ CREATE INDEX imap_sync_index ON imap_sync(transport_id, folder);
14391439
.await?;
14401440
}
14411441

1442+
inc_and_check(&mut migration_version, 142)?;
1443+
if dbversion < migration_version {
1444+
sql.execute_migration(
1445+
"ALTER TABLE transports
1446+
ADD COLUMN add_timestamp INTEGER NOT NULL DEFAULT 0;
1447+
CREATE TABLE removed_transports (
1448+
addr TEXT NOT NULL,
1449+
remove_timestamp INTEGER NOT NULL,
1450+
UNIQUE(addr)
1451+
) STRICT;",
1452+
migration_version,
1453+
)
1454+
.await?;
1455+
}
1456+
14421457
let new_version = sql
14431458
.get_raw_config_int(VERSION_CFG)
14441459
.await?

src/sync.rs

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@ use crate::config::Config;
99
use crate::constants::Blocked;
1010
use crate::contact::ContactId;
1111
use crate::context::Context;
12-
use crate::log::LogExt;
13-
use crate::log::warn;
12+
use crate::log::{LogExt as _, warn};
13+
use crate::login_param::EnteredLoginParam;
1414
use crate::message::{Message, MsgId, Viewtype};
1515
use crate::mimeparser::SystemMessage;
1616
use crate::param::Param;
1717
use crate::sync::SyncData::{AddQrToken, AlterChat, DeleteQrToken};
1818
use crate::token::Namespace;
1919
use crate::tools::time;
20+
use crate::transport::{ConfiguredLoginParamJson, sync_transports};
2021
use crate::{message, stock_str, token};
2122
use std::collections::HashSet;
2223

@@ -52,6 +53,29 @@ pub(crate) struct QrTokenData {
5253
pub(crate) grpid: Option<String>,
5354
}
5455

56+
#[derive(Debug, Serialize, Deserialize)]
57+
pub(crate) struct TransportData {
58+
/// Configured login parameters.
59+
pub(crate) configured: ConfiguredLoginParamJson,
60+
61+
/// Login parameters entered by the user.
62+
///
63+
/// They can be used to reconfigure the transport.
64+
pub(crate) entered: EnteredLoginParam,
65+
66+
/// Timestamp of when the transport was last time (re)configured.
67+
pub(crate) timestamp: i64,
68+
}
69+
70+
#[derive(Debug, Serialize, Deserialize)]
71+
pub(crate) struct RemovedTransportData {
72+
/// Address of the removed transport.
73+
pub(crate) addr: String,
74+
75+
/// Timestamp of when the transport was removed.
76+
pub(crate) timestamp: i64,
77+
}
78+
5579
#[derive(Debug, Serialize, Deserialize)]
5680
pub(crate) enum SyncData {
5781
AddQrToken(QrTokenData),
@@ -71,6 +95,28 @@ pub(crate) enum SyncData {
7195
DeleteMessages {
7296
msgs: Vec<String>, // RFC724 id (i.e. "Message-Id" header)
7397
},
98+
99+
/// Update transport configuration.
100+
///
101+
/// This message contains a list of all added transports
102+
/// together with their addition timestamp,
103+
/// and all removed transports together with
104+
/// the removal timestamp.
105+
///
106+
/// In case of a tie, addition and removal timestamps
107+
/// being the same, removal wins.
108+
/// It is more likely that transport is added
109+
/// and then removed within a second,
110+
/// but unlikely the other way round
111+
/// as adding new transport takes time
112+
/// to run configuration.
113+
Transports {
114+
/// Active transports.
115+
transports: Vec<TransportData>,
116+
117+
/// Removed transports with the timestamp of removal.
118+
removed_transports: Vec<RemovedTransportData>,
119+
},
74120
}
75121

76122
#[derive(Debug, Serialize, Deserialize)]
@@ -274,6 +320,10 @@ impl Context {
274320
SyncData::Config { key, val } => self.sync_config(key, val).await,
275321
SyncData::SaveMessage { src, dest } => self.save_message(src, dest).await,
276322
SyncData::DeleteMessages { msgs } => self.sync_message_deletion(msgs).await,
323+
SyncData::Transports {
324+
transports,
325+
removed_transports,
326+
} => sync_transports(self, transports, removed_transports).await,
277327
},
278328
SyncDataOrUnknown::Unknown(data) => {
279329
warn!(self, "Ignored unknown sync item: {data}.");

src/test_utils.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -600,7 +600,7 @@ impl TestContext {
600600
self.ctx
601601
.set_config(Config::ConfiguredAddr, Some(addr))
602602
.await
603-
.unwrap();
603+
.expect("Failed to configure address");
604604

605605
if let Some(name) = addr.split('@').next() {
606606
self.set_name(name);

0 commit comments

Comments
 (0)