Skip to content

Commit 7ec3e47

Browse files
committed
feat: synchronize transports via sync messages
1 parent 913e883 commit 7ec3e47

File tree

8 files changed

+309
-56
lines changed

8 files changed

+309
-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, 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+
sync_transports(self).await?;
248+
239249
Ok(())
240250
}
241251

@@ -553,7 +563,8 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
553563

554564
progress!(ctx, 900);
555565

556-
if !ctx.is_configured().await? {
566+
let is_configured = ctx.is_configured().await?;
567+
if !is_configured {
557568
ctx.sql.set_raw_config("mvbox_move", Some("0")).await?;
558569
ctx.sql.set_raw_config("only_fetch_mvbox", None).await?;
559570
}
@@ -568,8 +579,10 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
568579

569580
let provider = configured_param.provider;
570581
configured_param
571-
.save_to_transports_table(ctx, param)
582+
.clone()
583+
.save_to_transports_table(ctx, param, time())
572584
.await?;
585+
sync_transports(ctx).await?;
573586

574587
ctx.set_config_internal(Config::ConfiguredTimestamp, Some(&time().to_string()))
575588
.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: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@ 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::events::EventType;
13+
use crate::log::{LogExt as _, warn};
14+
use crate::login_param::EnteredLoginParam;
1415
use crate::message::{Message, MsgId, Viewtype};
1516
use crate::mimeparser::SystemMessage;
1617
use crate::param::Param;
1718
use crate::sync::SyncData::{AddQrToken, AlterChat, DeleteQrToken};
1819
use crate::token::Namespace;
1920
use crate::tools::time;
21+
use crate::transport::{ConfiguredLoginParamJson, save_transport};
2022
use crate::{message, stock_str, token};
2123
use std::collections::HashSet;
2224

@@ -52,6 +54,29 @@ pub(crate) struct QrTokenData {
5254
pub(crate) grpid: Option<String>,
5355
}
5456

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

76123
#[derive(Debug, Serialize, Deserialize)]
@@ -274,6 +321,10 @@ impl Context {
274321
SyncData::Config { key, val } => self.sync_config(key, val).await,
275322
SyncData::SaveMessage { src, dest } => self.save_message(src, dest).await,
276323
SyncData::DeleteMessages { msgs } => self.sync_message_deletion(msgs).await,
324+
SyncData::Transports {
325+
transports,
326+
removed_transports,
327+
} => self.sync_transports(transports, removed_transports).await,
277328
},
278329
SyncDataOrUnknown::Unknown(data) => {
279330
warn!(self, "Ignored unknown sync item: {data}.");
@@ -347,6 +398,46 @@ impl Context {
347398
message::delete_msgs_locally_done(self, &msg_ids, modified_chat_ids).await?;
348399
Ok(())
349400
}
401+
402+
/// Process received data for transport synchronization.
403+
async fn sync_transports(
404+
&self,
405+
transports: &[TransportData],
406+
removed_transports: &[RemovedTransportData],
407+
) -> Result<()> {
408+
for TransportData {
409+
configured,
410+
entered,
411+
timestamp,
412+
} in transports
413+
{
414+
save_transport(self, entered, configured, *timestamp).await?;
415+
}
416+
417+
self.sql
418+
.transaction(|transaction| {
419+
for RemovedTransportData { addr, timestamp } in removed_transports {
420+
transaction.execute(
421+
"DELETE FROM transports
422+
WHERE addr=? AND add_timestamp<=?",
423+
(addr, timestamp),
424+
)?;
425+
transaction.execute(
426+
"INSERT INTO removed_transports (addr, remove_timestamp)
427+
VALUES (?, ?)
428+
ON CONFLICT (addr) DO
429+
UPDATE SET remove_timestamp = excluded.remove_timestamp
430+
WHERE excluded.remove_timestamp > remove_timestamp",
431+
(addr, timestamp),
432+
)?;
433+
}
434+
Ok(())
435+
})
436+
.await?;
437+
438+
self.emit_event(EventType::TransportsModified);
439+
Ok(())
440+
}
350441
}
351442

352443
#[cfg(test)]

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)