Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f81c190
feat: restart I/O when setting configured_addr
link2xt Nov 5, 2025
1565d63
refactor: return transport ID from ConfiguredLoginParam::load()
link2xt Nov 1, 2025
4ce6360
feat: allow adding second transport
link2xt Oct 24, 2025
987997a
feat: allow deleting transports
link2xt Oct 25, 2025
db1a418
feat: allow to set another transport as primary
link2xt Oct 26, 2025
da6b8ee
feat: add transport column to imap table
link2xt Oct 31, 2025
5bf90ec
feat: add transport column to imap_sync table
link2xt Oct 31, 2025
e48b9df
api: add count_transports()
link2xt Oct 31, 2025
199bbed
feat: require mvbox_move and only_fetch_mvbox to be disabled for mult…
link2xt Oct 31, 2025
6812318
load transport ID with ConfiguredLoginParam
link2xt Nov 1, 2025
d3cf361
test: test that second transport cannot be set up if mvbox is used
link2xt Nov 1, 2025
73a5e12
test: current transport cannot be deleted after changing to it
link2xt Nov 2, 2025
e531a83
fix: do not allow to set ConfiguredAddr to arbitrary values
link2xt Nov 2, 2025
5d09039
ConfiguredLoginParam::load_all()
link2xt Nov 3, 2025
b9d52c1
Imap::new() accepting login params
link2xt Nov 3, 2025
3fe523f
start IMAP loops for all transports
link2xt Nov 3, 2025
9be79e0
test that own vcard contains the current primary transport address
link2xt Nov 3, 2025
85f2416
test: mvbox_move is disabled when first transport is set up
link2xt Nov 3, 2025
b513007
test: wait until account is connected in resetup_account()
link2xt Nov 5, 2025
ef3de16
Delete imap and imap_sync rows when the transport is deleted
link2xt Nov 5, 2025
a25c816
Never create mvbox when configuring a new transport
link2xt Nov 7, 2025
11a7933
store transport ID for IMAP session
link2xt Nov 3, 2025
c441ae4
log transport id on UID validity change
link2xt Nov 5, 2025
6a9c3ba
transport_id should be known when selecting folders
link2xt Nov 7, 2025
37bb164
Do not try to get UID validity of inbox during configuration
link2xt Nov 9, 2025
4009ecf
bring accounts online in wait_next_messages test
link2xt Nov 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions deltachat-rpc-client/src/deltachat_rpc_client/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ def add_transport_from_qr(self, qr: str):
"""Add a new transport using a QR code."""
yield self._rpc.add_transport_from_qr.future(self.id, qr)

def delete_transport(self, addr: str):
"""Delete a transport."""
self._rpc.delete_transport(self.id, addr)

@futuremethod
def list_transports(self):
"""Return the list of all email accounts that are used as a transport in the current profile."""
Expand Down
10 changes: 8 additions & 2 deletions deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,17 @@ def get_credentials(self) -> (str, str):
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
return f"{username}@{domain}", f"{username}${username}"

def get_account_qr(self):
"""Return "dcaccount:" QR code for testing chatmail relay."""
domain = os.getenv("CHATMAIL_DOMAIN")
return f"dcaccount:{domain}"

@futuremethod
def new_configured_account(self):
"""Create a new configured account."""
account = self.get_unconfigured_account()
domain = os.getenv("CHATMAIL_DOMAIN")
yield account.add_transport_from_qr.future(f"dcaccount:{domain}")
qr = self.get_account_qr()
yield account.add_transport_from_qr.future(qr)

assert account.is_configured()
return account
Expand Down Expand Up @@ -77,6 +82,7 @@ def resetup_account(self, ac: Account) -> Account:
ac_clone = self.get_unconfigured_account()
for transport in transports:
ac_clone.add_or_update_transport(transport)
ac_clone.bring_online()
return ac_clone

def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:
Expand Down
5 changes: 4 additions & 1 deletion deltachat-rpc-client/tests/test_folders.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,10 @@ def test_delete_deltachat_folder(acfactory, direct_imap):
# Wait until new folder is created and UIDVALIDITY is updated.
while True:
event = ac1.wait_for_event()
if event.kind == EventType.INFO and "uid/validity change folder DeltaChat" in event.msg:
if (
event.kind == EventType.INFO
and "UID validity for folder DeltaChat and transport 1 changed from " in event.msg
):
break

ac2 = acfactory.get_online_account()
Expand Down
120 changes: 120 additions & 0 deletions deltachat-rpc-client/tests/test_multitransport.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import pytest

from deltachat_rpc_client.rpc import JsonRpcError


def test_add_second_address(acfactory) -> None:
account = acfactory.new_configured_account()
assert len(account.list_transports()) == 1

# When the first transport is created,
# mvbox_move and only_fetch_mvbox should be disabled.
assert account.get_config("mvbox_move") == "0"
assert account.get_config("only_fetch_mvbox") == "0"
assert account.get_config("show_emails") == "2"

qr = acfactory.get_account_qr()
account.add_transport_from_qr(qr)
assert len(account.list_transports()) == 2

account.add_transport_from_qr(qr)
assert len(account.list_transports()) == 3

first_addr = account.list_transports()[0]["addr"]
second_addr = account.list_transports()[1]["addr"]

# Cannot delete the first address.
with pytest.raises(JsonRpcError):
account.delete_transport(first_addr)

account.delete_transport(second_addr)
assert len(account.list_transports()) == 2

# Enabling mvbox_move or only_fetch_mvbox
# is not allowed when multi-transport is enabled.
for option in ["mvbox_move", "only_fetch_mvbox"]:
with pytest.raises(JsonRpcError):
account.set_config(option, "1")


@pytest.mark.parametrize("key", ["mvbox_move", "only_fetch_mvbox"])
def test_no_second_transport_with_mvbox(acfactory, key) -> None:
"""Test that second transport cannot be configured if mvbox is used."""
account = acfactory.new_configured_account()
assert len(account.list_transports()) == 1

assert account.get_config("mvbox_move") == "0"
assert account.get_config("only_fetch_mvbox") == "0"

qr = acfactory.get_account_qr()
account.set_config(key, "1")

with pytest.raises(JsonRpcError):
account.add_transport_from_qr(qr)


def test_change_address(acfactory) -> None:
"""Test Alice configuring a second transport and setting it as a primary one."""
alice, bob = acfactory.get_online_accounts(2)

bob_addr = bob.get_config("configured_addr")
bob.create_chat(alice)

alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.send_text("Hello!")

msg1 = bob.wait_for_incoming_msg().get_snapshot()
sender_addr1 = msg1.sender.get_snapshot().address

alice.stop_io()
old_alice_addr = alice.get_config("configured_addr")
alice_vcard = alice.self_contact.make_vcard()
assert old_alice_addr in alice_vcard
qr = acfactory.get_account_qr()
alice.add_transport_from_qr(qr)
new_alice_addr = alice.list_transports()[1]["addr"]
with pytest.raises(JsonRpcError):
# Cannot use the address that is not
# configured for any transport.
alice.set_config("configured_addr", bob_addr)
alice.set_config("configured_addr", new_alice_addr)
alice_vcard = alice.self_contact.make_vcard()
assert old_alice_addr not in alice_vcard
assert new_alice_addr in alice_vcard
with pytest.raises(JsonRpcError):
alice.delete_transport(new_alice_addr)
alice.start_io()

alice_chat_bob.send_text("Hello again!")

msg2 = bob.wait_for_incoming_msg().get_snapshot()
sender_addr2 = msg2.sender.get_snapshot().address

assert msg1.sender == msg2.sender
assert sender_addr1 != sender_addr2
assert sender_addr1 == old_alice_addr
assert sender_addr2 == new_alice_addr


@pytest.mark.parametrize("is_chatmail", ["0", "1"])
def test_mvbox_move_first_transport(acfactory, is_chatmail) -> None:
"""Test that mvbox_move is disabled by default even for non-chatmail accounts.
Disabling mvbox_move is required to be able to setup a second transport.
"""
account = acfactory.get_unconfigured_account()

account.set_config("fix_is_chatmail", "1")
account.set_config("is_chatmail", is_chatmail)

# The default value when the setting is unset is "1".
# This is not changed for compatibility with old databases
# imported from backups.
assert account.get_config("mvbox_move") == "1"

qr = acfactory.get_account_qr()
account.add_transport_from_qr(qr)

# Once the first transport is set up,
# mvbox_move is disabled.
assert account.get_config("mvbox_move") == "0"
assert account.get_config("is_chatmail") == is_chatmail
3 changes: 2 additions & 1 deletion deltachat-rpc-client/tests/test_something.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,14 +472,15 @@ def track(e):


def test_wait_next_messages(acfactory) -> None:
alice = acfactory.new_configured_account()
alice = acfactory.get_online_account()

# Create a bot account so it does not receive device messages in the beginning.
addr, password = acfactory.get_credentials()
bot = acfactory.get_unconfigured_account()
bot.set_config("bot", "1")
bot.add_or_update_transport({"addr": addr, "password": password})
assert bot.is_configured()
bot.bring_online()

# There are no old messages and the call returns immediately.
assert not bot.wait_next_messages()
Expand Down
53 changes: 40 additions & 13 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,10 @@ impl Config {

/// Whether the config option needs an IO scheduler restart to take effect.
pub(crate) fn needs_io_restart(&self) -> bool {
matches!(self, Config::MvboxMove | Config::OnlyFetchMvbox)
matches!(
self,
Config::MvboxMove | Config::OnlyFetchMvbox | Config::ConfiguredAddr
)
}
}

Expand Down Expand Up @@ -706,6 +709,11 @@ impl Context {
pub async fn set_config(&self, key: Config, value: Option<&str>) -> Result<()> {
Self::check_config(key, value)?;

let n_transports = self.count_transports().await?;
if n_transports > 1 && matches!(key, Config::MvboxMove | Config::OnlyFetchMvbox) {
bail!("Cannot reconfigure {key} when multiple transports are configured");
}

let _pause = match key.needs_io_restart() {
true => self.scheduler.pause(self).await?,
_ => Default::default(),
Expand Down Expand Up @@ -791,20 +799,39 @@ impl Context {
.await?;
}
Config::ConfiguredAddr => {
if self.is_configured().await? {
bail!("Cannot change ConfiguredAddr");
if !self.is_configured().await? {
if let Some(addr) = value {
info!(
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?;
}
}
if let Some(addr) = value {
info!(
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())
self.sql
.transaction(|transaction| {
if transaction.query_row(
"SELECT COUNT(*) FROM transports WHERE addr=?",
(value,),
|row| {
let res: i64 = row.get(0)?;
Ok(res)
},
)? == 0
{
bail!("Address does not belong to any transport.");
}
transaction.execute(
"UPDATE config SET value=? WHERE keyname='configured_addr'",
(value,),
)?;
Ok(())
})
.await?;
}
}
_ => {
self.sql.set_raw_config(key.as_ref(), value).await?;
Expand Down
Loading