Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
55 changes: 45 additions & 10 deletions deltachat-rpc-client/tests/test_multitransport.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,34 +315,69 @@ def test_transport_limit(acfactory) -> None:
account.add_transport_from_qr(qr)


def test_message_info_imap_urls(acfactory, log) -> None:
def test_message_info_imap_urls(acfactory) -> None:
"""Test that message info contains IMAP URLs of where the message was received."""
alice, bob = acfactory.get_online_accounts(2)

log.section("Alice adds ac1 clone removes second transport")
qr = acfactory.get_account_qr()
for i in range(3):
alice.add_transport_from_qr(qr)
# Wait for all transports to go IDLE after adding each one.
for _ in range(i + 1):
alice.bring_online()

new_alice_addr = alice.list_transports()[2]["addr"]
alice.set_config("configured_addr", new_alice_addr)

# Enable multi-device mode so messages are not deleted immediately.
alice.set_config("bcc_self", "1")

# Bob creates chat, learning about Alice's currently selected transport.
# This is where he will send the message.
bob_chat = bob.create_chat(alice)

# Alice changes the transport again.
alice.set_config("configured_addr", alice.list_transports()[3]["addr"])
# 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:
alice.delete_transport(transport["addr"])
removed_addrs.append(transport["addr"])
alice.stop_io()
alice.start_io()

bob_chat.send_text("Hello!")

msg = alice.wait_for_incoming_msg()
for alice_transport in alice.list_transports():
addr = alice_transport["addr"]
assert (addr == new_alice_addr) == (addr in msg.get_info())
msg_info = msg.get_info()
assert new_alice_addr in msg_info
for removed_addr in removed_addrs:
assert removed_addr not in msg_info
assert f"{new_alice_addr}/INBOX" in msg_info


def test_remove_primary_transport(acfactory) -> None:
"""Test that after removing the primary relay, Alice can still receive messages."""
alice, bob = acfactory.get_online_accounts(2)
qr = acfactory.get_account_qr()

alice.add_transport_from_qr(qr)
alice.bring_online()

bob_chat = bob.create_chat(alice)
alice.create_chat(bob)

# Alice changes the transport.
[transport1, transport2] = alice.list_transports()
alice.set_config("configured_addr", transport2["addr"])

bob_chat.send_text("Hello!")
msg1 = alice.wait_for_incoming_msg().get_snapshot()
assert msg1.text == "Hello!"

# Alice deletes the first transport.
alice.delete_transport(transport1["addr"])
alice.stop_io()
alice.start_io()

bob_chat.send_text("Hello again!")
msg2 = alice.wait_for_incoming_msg().get_snapshot()
assert msg2.text == "Hello again!"
10 changes: 8 additions & 2 deletions src/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ use crate::message::{self, Message, MessageState, MsgId, Viewtype};
use crate::mimefactory::{MimeFactory, RenderedEmail};
use crate::mimeparser::SystemMessage;
use crate::param::{Param, Params};
use crate::pgp::addresses_from_public_key;
use crate::receive_imf::ReceivedMsg;
use crate::smtp::{self, send_msg_to_smtp};
use crate::stock_str;
Expand Down Expand Up @@ -1174,8 +1175,13 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
let fingerprint = contact
.fingerprint()
.context("Contact does not have a fingerprint in encrypted chat")?;
if contact.public_key(context).await?.is_some() {
ret += &format!("\n{addr}\n{fingerprint}\n");
if let Some(public_key) = contact.public_key(context).await? {
if let Some(relay_addrs) = addresses_from_public_key(&public_key) {
let relays = relay_addrs.join(",");
ret += &format!("\n{addr}({relays})\n{fingerprint}\n");
} else {
ret += &format!("\n{addr}\n{fingerprint}\n");
}
} else {
ret += &format!("\n{addr}\n(key missing)\n{fingerprint}\n");
}
Expand Down
7 changes: 4 additions & 3 deletions src/chat/chat_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3856,6 +3856,7 @@ async fn test_only_broadcast_owner_can_send_2() -> Result<()> {
tcm.section("Now, Alice's fingerprint changes");

alice.sql.execute("DELETE FROM keypairs", ()).await?;
*alice.self_public_key.lock().await = None;
alice
.sql
.execute("DELETE FROM config WHERE keyname='key_id'", ())
Expand Down Expand Up @@ -4038,7 +4039,7 @@ async fn test_chat_get_encryption_info() -> Result<()> {
chat_id.get_encryption_info(alice).await?,
"Messages are end-to-end encrypted.\n\
\n\
bob@example.net\n\
bob@example.net(bob@example.net)\n\
CCCB 5AA9 F6E1 141C 9431\n\
65F1 DB18 B18C BCF7 0487"
);
Expand All @@ -4048,11 +4049,11 @@ async fn test_chat_get_encryption_info() -> Result<()> {
chat_id.get_encryption_info(alice).await?,
"Messages are end-to-end encrypted.\n\
\n\
fiona@example.net\n\
fiona@example.net(fiona@example.net)\n\
C8BA 50BF 4AC1 2FAF 38D7\n\
F657 DDFC 8E9F 3C79 9195\n\
\n\
bob@example.net\n\
bob@example.net(bob@example.net)\n\
CCCB 5AA9 F6E1 141C 9431\n\
65F1 DB18 B18C BCF7 0487"
);
Expand Down
34 changes: 28 additions & 6 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use crate::log::LogExt;
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::provider::Provider;
use crate::sync::{self, Sync::*, SyncData};
use crate::tools::get_abs_path;
use crate::tools::{get_abs_path, time};
use crate::transport::{ConfiguredLoginParam, add_pseudo_transport, send_sync_transports};
use crate::{constants, stats};

Expand Down Expand Up @@ -828,6 +828,22 @@ impl Context {
(addr,),
)?;

// Update the timestamp for the primary transport
// so it becomes the first in `get_all_self_addrs()` list
// and the list of relays distributed in the public key.
// This ensures that messages will be sent
// to the primary relay by the contacts
// and will be fetched in background_fetch()
// which only fetches from the primary transport.
transaction
.execute(
"UPDATE transports SET add_timestamp=? WHERE addr=?",
(time(), addr),
)
.context(
"Failed to update add_timestamp for the new primary transport",
)?;

// Clean up SMTP and IMAP APPEND queue.
//
// The messages in the queue have a different
Expand Down Expand Up @@ -944,12 +960,18 @@ impl Context {
Ok(())
}

/// Returns the primary self address followed by all secondary ones.
/// Returns all self addresses, newest first.
pub(crate) async fn get_all_self_addrs(&self) -> Result<Vec<String>> {
let primary_addrs = self.get_config(Config::ConfiguredAddr).await?.into_iter();
let secondary_addrs = self.get_secondary_self_addrs().await?.into_iter();

Ok(primary_addrs.chain(secondary_addrs).collect())
self.sql
.query_map_vec(
"SELECT addr FROM transports ORDER BY add_timestamp DESC",
(),
|row| {
let addr: String = row.get(0)?;
Ok(addr)
},
)
.await
}

/// Returns all secondary self addresses.
Expand Down
7 changes: 3 additions & 4 deletions src/configure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -549,9 +549,6 @@ async fn get_configured_param(
async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'static Provider>> {
progress!(ctx, 1);

let ctx2 = ctx.clone();
let update_device_chats_handle = task::spawn(async move { ctx2.update_device_chats().await });

let configured_param = get_configured_param(ctx, param).await?;
let proxy_config = ProxyConfig::load(ctx).await?;
let strict_tls = configured_param.strict_tls(proxy_config.is_some());
Expand Down Expand Up @@ -642,7 +639,9 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
ctx.scheduler.interrupt_inbox().await;

progress!(ctx, 940);
update_device_chats_handle.await??;
ctx.update_device_chats()
.await
.context("Failed to update device chats")?;

ctx.sql.set_raw_config_bool("configured", true).await?;
ctx.emit_event(EventType::AccountsItemChanged);
Expand Down
95 changes: 79 additions & 16 deletions src/contact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ use crate::log::{LogExt, warn};
use crate::message::MessageState;
use crate::mimeparser::AvatarAction;
use crate::param::{Param, Params};
use crate::pgp::{addresses_from_public_key, merge_openpgp_certificates};
use crate::sync::{self, Sync::*};
use crate::tools::{SystemTime, duration_to_str, get_abs_path, normalize_text, time, to_lowercase};
use crate::{chat, chatlist_events, ensure_and_debug_assert_ne, stock_str};
Expand Down Expand Up @@ -314,6 +315,67 @@ pub async fn make_vcard(context: &Context, contacts: &[ContactId]) -> Result<Str
.to_string())
}

/// Imports public key into the public key store.
///
/// They key may come from Autocrypt header,
/// Autocrypt-Gossip header or a vCard.
///
/// If the key with the same fingerprint already exists,
/// it is updated by merging the new key.
pub(crate) async fn import_public_key(
context: &Context,
public_key: &SignedPublicKey,
) -> Result<()> {
public_key
.verify_bindings()
.context("Attempt to import broken public key")?;

let fingerprint = public_key.dc_fingerprint().hex();

let merged_public_key;
let merged_public_key_ref = if let Some(public_key_bytes) = context
.sql
.query_row_optional(
"SELECT public_key
FROM public_keys
WHERE fingerprint=?",
(&fingerprint,),
|row| {
let bytes: Vec<u8> = row.get(0)?;
Ok(bytes)
},
)
.await?
{
let old_public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
merged_public_key = merge_openpgp_certificates(public_key.clone(), old_public_key)
.context("Failed to merge public keys")?;
&merged_public_key
} else {
public_key
};

let inserted = context
.sql
.execute(
"INSERT INTO public_keys (fingerprint, public_key)
VALUES (?, ?)
ON CONFLICT (fingerprint)
DO UPDATE SET public_key=excluded.public_key
WHERE public_key!=excluded.public_key",
(&fingerprint, merged_public_key_ref.to_bytes()),
)
.await?;
if inserted > 0 {
info!(
context,
"Saved key with fingerprint {fingerprint} from the Autocrypt header"
);
}

Ok(())
}

/// Imports contacts from the given vCard.
///
/// Returns the ids of successfully processed contacts in the order they appear in `vcard`,
Expand Down Expand Up @@ -352,23 +414,14 @@ async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Resu
.ok()
});

let fingerprint;
if let Some(public_key) = key {
fingerprint = public_key.dc_fingerprint().hex();

context
.sql
.execute(
"INSERT INTO public_keys (fingerprint, public_key)
VALUES (?, ?)
ON CONFLICT (fingerprint)
DO NOTHING",
(&fingerprint, public_key.to_bytes()),
)
.await?;
let fingerprint = if let Some(public_key) = key {
import_public_key(context, &public_key)
.await
.context("Failed to import public key from vCard")?;
public_key.dc_fingerprint().hex()
} else {
fingerprint = String::new();
}
String::new()
};

let (id, modified) =
match Contact::add_or_lookup_ex(context, &contact.authname, &addr, &fingerprint, origin)
Expand Down Expand Up @@ -1384,6 +1437,16 @@ WHERE addr=?
);
}

if let Some(public_key) = contact.public_key(context).await?
&& let Some(relay_addrs) = addresses_from_public_key(&public_key)
{
ret += "\n\nRelays:";
for relay in &relay_addrs {
ret += "\n";
ret += relay;
}
}

Ok(ret)
}

Expand Down
5 changes: 4 additions & 1 deletion src/contact/contact_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -841,7 +841,10 @@ Me (alice@example.org):

bob@example.net (bob@example.net):
CCCB 5AA9 F6E1 141C 9431
65F1 DB18 B18C BCF7 0487"
65F1 DB18 B18C BCF7 0487

Relays:
bob@example.net"
);
let contact = Contact::get_by_id(alice, contact_bob_id).await?;
assert!(contact.e2ee_avail(alice).await?);
Expand Down
Loading
Loading