From 93539b75f8a79a26940e6c239c813de6947d4425 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 10 Apr 2025 15:11:25 +0000 Subject: [PATCH 001/381] api!: remove reset_encryption() APIs --- deltachat-jsonrpc/src/api.rs | 9 --- .../src/deltachat_rpc_client/contact.py | 4 -- deltachat-rpc-client/tests/test_something.py | 1 - src/contact.rs | 25 -------- src/contact/contact_tests.rs | 57 ------------------- 5 files changed, 96 deletions(-) diff --git a/deltachat-jsonrpc/src/api.rs b/deltachat-jsonrpc/src/api.rs index bdab3bba6b..e7fed37380 100644 --- a/deltachat-jsonrpc/src/api.rs +++ b/deltachat-jsonrpc/src/api.rs @@ -1542,15 +1542,6 @@ impl CommandApi { Ok(()) } - /// Resets contact encryption. - async fn reset_contact_encryption(&self, account_id: u32, contact_id: u32) -> Result<()> { - let ctx = self.get_context(account_id).await?; - let contact_id = ContactId::new(contact_id); - - contact_id.reset_encryption(&ctx).await?; - Ok(()) - } - /// Sets display name for existing contact. async fn change_contact_name( &self, diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/contact.py b/deltachat-rpc-client/src/deltachat_rpc_client/contact.py index a03d6ea2dc..b1d9d4d4e2 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/contact.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/contact.py @@ -36,10 +36,6 @@ def delete(self) -> None: """Delete contact.""" self._rpc.delete_contact(self.account.id, self.id) - def reset_encryption(self) -> None: - """Reset contact encryption.""" - self._rpc.reset_contact_encryption(self.account.id, self.id) - def set_name(self, name: str) -> None: """Change the name of this contact.""" self._rpc.change_contact_name(self.account.id, self.id, name) diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index d859140f71..93d96fdd2e 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -287,7 +287,6 @@ def test_contact(acfactory) -> None: assert repr(alice_contact_bob) alice_contact_bob.block() alice_contact_bob.unblock() - alice_contact_bob.reset_encryption() alice_contact_bob.set_name("new name") alice_contact_bob.get_encryption_info() snapshot = alice_contact_bob.get_snapshot() diff --git a/src/contact.rs b/src/contact.rs index 08174f9e00..67eba883e2 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -196,31 +196,6 @@ impl ContactId { .await?; Ok(addr) } - - /// Resets encryption with the contact. - /// - /// Effect is similar to receiving a message without Autocrypt header - /// from the contact, but this action is triggered manually by the user. - /// - /// For example, this will result in sending the next message - /// to 1:1 chat unencrypted, but will not remove existing verified keys. - pub async fn reset_encryption(self, context: &Context) -> Result<()> { - let now = time(); - - let addr = self.addr(context).await?; - if let Some(mut peerstate) = Peerstate::from_addr(context, &addr).await? { - peerstate.degrade_encryption(now); - peerstate.save_to_db(&context.sql).await?; - } - - // Reset 1:1 chat protection. - if let Some(chat_id) = ChatId::lookup_by_contact(context, self).await? { - chat_id - .set_protection(context, ProtectionStatus::Unprotected, now, Some(self)) - .await?; - } - Ok(()) - } } impl fmt::Display for ContactId { diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs index 98724cc9d1..1e28b00f62 100644 --- a/src/contact/contact_tests.rs +++ b/src/contact/contact_tests.rs @@ -1202,63 +1202,6 @@ async fn test_import_vcard_updates_only_key() -> Result<()> { Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_reset_encryption() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = &tcm.alice().await; - let bob = &tcm.bob().await; - - let msg = tcm.send_recv_accept(bob, alice, "Hi!").await; - assert_eq!(msg.get_showpadlock(), true); - - let alice_bob_chat_id = msg.chat_id; - let alice_bob_contact_id = msg.from_id; - - alice_bob_contact_id.reset_encryption(alice).await?; - - let sent = alice.send_text(alice_bob_chat_id, "Unencrypted").await; - let msg = bob.recv_msg(&sent).await; - assert_eq!(msg.get_showpadlock(), false); - - Ok(()) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_reset_verified_encryption() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = &tcm.alice().await; - let bob = &tcm.bob().await; - - tcm.execute_securejoin(bob, alice).await; - - let msg = tcm.send_recv(bob, alice, "Encrypted").await; - assert_eq!(msg.get_showpadlock(), true); - - let alice_bob_chat_id = msg.chat_id; - let alice_bob_contact_id = msg.from_id; - - alice_bob_contact_id.reset_encryption(alice).await?; - - // Check that the contact is still verified after resetting encryption. - let alice_bob_contact = Contact::get_by_id(alice, alice_bob_contact_id).await?; - assert_eq!(alice_bob_contact.is_verified(alice).await?, true); - - // 1:1 chat and profile is no longer verified. - assert_eq!(alice_bob_contact.is_profile_verified(alice).await?, false); - - let info_msg = alice.get_last_msg_in(alice_bob_chat_id).await; - assert_eq!( - info_msg.text, - "bob@example.net sent a message from another device." - ); - - let sent = alice.send_text(alice_bob_chat_id, "Unencrypted").await; - let msg = bob.recv_msg(&sent).await; - assert_eq!(msg.get_showpadlock(), false); - - Ok(()) -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_self_is_verified() -> Result<()> { let mut tcm = TestContextManager::new(); From 65f529c769125dcaa8cc89906a75b8851a25e91e Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 28 Feb 2025 18:47:26 +0000 Subject: [PATCH 002/381] PGP-contacts --- src/chat.rs | 34 +++- src/chat/chat_tests.rs | 20 +- src/chatlist.rs | 4 +- src/contact.rs | 151 +++++++++++---- src/contact/contact_tests.rs | 37 +++- src/decrypt.rs | 13 -- src/events/chatlist_events.rs | 3 +- src/imap.rs | 4 +- src/key.rs | 24 ++- src/message/message_tests.rs | 2 +- src/mimeparser.rs | 31 +-- src/peerstate.rs | 181 +----------------- src/qr.rs | 13 +- src/qr/qr_tests.rs | 12 +- src/receive_imf.rs | 130 +++++++++---- src/receive_imf/receive_imf_tests.rs | 4 - src/securejoin.rs | 1 + src/securejoin/securejoin_tests.rs | 22 +-- src/sql.rs | 30 +-- src/sql/migrations.rs | 44 ++++- src/sql/sql_tests.rs | 4 +- src/stock_str/stock_str_tests.rs | 2 +- src/test_utils.rs | 64 ++++++- src/tests/verified_chats.rs | 20 +- test-data/golden/test_old_message_1 | 2 +- test-data/golden/test_old_message_2 | 2 +- test-data/golden/test_old_message_3 | 2 +- test-data/golden/test_outgoing_mua_msg | 2 +- ...test_verified_oneonone_chat_enable_disable | 2 +- .../verified_chats_message_from_old_dc_setup | 2 +- 30 files changed, 451 insertions(+), 411 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index 2e68bc4cd8..4ae93608bc 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -2317,7 +2317,11 @@ impl Chat { return Ok(None); } let contact = Contact::get_by_id(context, contact_id).await?; - r = Some(SyncId::ContactAddr(contact.get_addr().to_string())); + if let Some(fingerprint) = contact.fingerprint() { + r = Some(SyncId::ContactFingerprint(fingerprint.to_string())); + } else { + r = Some(SyncId::ContactAddr(contact.get_addr().to_string())); + } } Ok(r) } @@ -4847,7 +4851,12 @@ async fn set_contacts_by_addrs(context: &Context, id: ChatId, addrs: &[String]) /// A cross-device chat id used for synchronisation. #[derive(Debug, Serialize, Deserialize, PartialEq)] pub(crate) enum SyncId { + // E-mail address of the contact. ContactAddr(String), + + // OpenPGP fingerprint of the contact. + ContactFingerprint(String), + Grpid(String), /// "Message-ID"-s, from oldest to latest. Used for ad-hoc groups. Msgids(Vec), @@ -4899,6 +4908,29 @@ impl Context { .await? .id } + SyncId::ContactFingerprint(fingerprint) => { + let name = ""; + let addr = ""; + let (contact_id, _) = + Contact::add_or_lookup_ex(self, name, addr, fingerprint, Origin::Hidden) + .await?; + match action { + SyncAction::Rename(to) => { + contact_id.set_name_ex(self, Nosync, to).await?; + return Ok(()); + } + SyncAction::Block => { + return contact::set_blocked(self, Nosync, contact_id, true).await + } + SyncAction::Unblock => { + return contact::set_blocked(self, Nosync, contact_id, false).await + } + _ => (), + } + ChatIdBlocked::get_for_contact(self, contact_id, Blocked::Request) + .await? + .id + } SyncId::Grpid(grpid) => { if let SyncAction::CreateBroadcast(name) = action { create_broadcast_list_ex(self, Nosync, grpid.clone(), name.clone()).await?; diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 37c983e737..c08e877947 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -43,7 +43,7 @@ async fn test_chat_info() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_get_draft_no_draft() { - let t = TestContext::new().await; + let t = TestContext::new_alice().await; let chat = t.get_self_chat().await; let draft = chat.id.get_draft(&t).await.unwrap(); assert!(draft.is_none()); @@ -60,14 +60,14 @@ async fn test_get_draft_special_chat_id() { async fn test_get_draft_no_chat() { // This is a weird case, maybe this should be an error but we // do not get this info from the database currently. - let t = TestContext::new().await; + let t = TestContext::new_alice().await; let draft = ChatId::new(42).get_draft(&t).await.unwrap(); assert!(draft.is_none()); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_get_draft() { - let t = TestContext::new().await; + let t = TestContext::new_alice().await; let chat_id = &t.get_self_chat().await.id; let mut msg = Message::new_text("hello".to_string()); @@ -968,7 +968,7 @@ async fn test_delete_device_chat() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_device_chat_cannot_sent() { - let t = TestContext::new().await; + let t = TestContext::new_alice().await; t.update_device_chats().await.unwrap(); let device_chat_id = ChatId::get_for_contact(&t, ContactId::DEVICE) .await @@ -1015,7 +1015,7 @@ async fn chatlist_len(ctx: &Context, listflags: usize) -> usize { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_archive() { // create two chats - let t = TestContext::new().await; + let t = TestContext::new_alice().await; let mut msg = Message::new_text("foo".to_string()); let msg_id = add_device_msg(&t, None, Some(&mut msg)).await.unwrap(); let chat_id1 = message::Message::load_from_db(&t, msg_id) @@ -1313,7 +1313,7 @@ async fn get_chats_from_chat_list(ctx: &Context, listflags: usize) -> Vec Result<()> { assert_eq!(alice1_contacts.len(), 1); let a1b_contact_id = alice1_contacts[0]; let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?; - assert_eq!(a1b_contact.get_addr(), "bob@example.net"); + assert_eq!(a1b_contact.get_addr(), ""); assert_eq!(a1b_contact.origin, Origin::CreateChat); - let a1b_chat = alice1.get_chat(bob).await; + let a1b_chat = alice1.get_pgp_chat(bob).await; assert_eq!(a1b_chat.blocked, Blocked::Not); let chats = Chatlist::try_load(alice1, 0, None, None).await?; assert_eq!(chats.len(), 1); @@ -3246,7 +3246,7 @@ async fn test_sync_muted() -> Result<()> { alice1.create_chat(&bob).await; assert_eq!( - alice1.get_chat(&bob).await.mute_duration, + alice1.get_pgp_chat(&bob).await.mute_duration, MuteDuration::NotMuted ); let mute_durations = [ @@ -3264,7 +3264,7 @@ async fn test_sync_muted() -> Result<()> { ), _ => m, }; - assert_eq!(alice1.get_chat(&bob).await.mute_duration, m); + assert_eq!(alice1.get_pgp_chat(&bob).await.mute_duration, m); } Ok(()) } diff --git a/src/chatlist.rs b/src/chatlist.rs index eb1f278b27..de58ef787c 100644 --- a/src/chatlist.rs +++ b/src/chatlist.rs @@ -571,7 +571,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_sort_self_talk_up_on_forward() { - let t = TestContext::new().await; + let t = TestContext::new_alice().await; t.update_device_chats().await.unwrap(); create_group_chat(&t, ProtectionStatus::Unprotected, "a chat") .await @@ -604,7 +604,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_search_special_chat_names() { - let t = TestContext::new().await; + let t = TestContext::new_alice().await; t.update_device_chats().await.unwrap(); let chats = Chatlist::try_load(&t, 0, Some("t-1234-s"), None) diff --git a/src/contact.rs b/src/contact.rs index 67eba883e2..2a8b4dc3fa 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -28,7 +28,7 @@ use crate::config::Config; use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY}; use crate::context::Context; use crate::events::EventType; -use crate::key::{load_self_public_key, DcKey, SignedPublicKey}; +use crate::key::{load_self_public_key, load_self_public_key_opt, DcKey, SignedPublicKey}; use crate::log::LogExt; use crate::message::MessageState; use crate::mimeparser::AvatarAction; @@ -102,7 +102,16 @@ impl ContactId { /// for this contact will switch to the /// contact's authorized name. pub async fn set_name(self, context: &Context, name: &str) -> Result<()> { - let addr = context + self.set_name_ex(context, Sync, name).await + } + + pub(crate) async fn set_name_ex( + self, + context: &Context, + sync: sync::Sync, + name: &str, + ) -> Result<()> { + let row = context .sql .transaction(|transaction| { let is_changed = transaction.execute( @@ -111,30 +120,44 @@ impl ContactId { )? > 0; if is_changed { update_chat_names(context, transaction, self)?; - let addr = transaction.query_row( - "SELECT addr FROM contacts WHERE id=?", + let (addr, fingerprint) = transaction.query_row( + "SELECT addr, fingerprint FROM contacts WHERE id=?", (self,), |row| { let addr: String = row.get(0)?; - Ok(addr) + let fingerprint: String = row.get(1)?; + Ok((addr, fingerprint)) }, )?; - Ok(Some(addr)) + Ok(Some((addr, fingerprint))) } else { Ok(None) } }) .await?; - if let Some(addr) = addr { - chat::sync( - context, - chat::SyncId::ContactAddr(addr.to_string()), - chat::SyncAction::Rename(name.to_string()), - ) - .await - .log_err(context) - .ok(); + if sync.into() { + if let Some((addr, fingerprint)) = row { + if fingerprint.is_empty() { + chat::sync( + context, + chat::SyncId::ContactAddr(addr), + chat::SyncAction::Rename(name.to_string()), + ) + .await + .log_err(context) + .ok(); + } else { + chat::sync( + context, + chat::SyncId::ContactFingerprint(fingerprint), + chat::SyncAction::Rename(name.to_string()), + ) + .await + .log_err(context) + .ok(); + } + } } Ok(()) } @@ -304,15 +327,6 @@ async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Resu // mustn't use `Origin::AddressBook` here because the vCard may be created not by us, also we // want `contact.authname` to be saved as the authname and not a locally given name. let origin = Origin::CreateChat; - let (id, modified) = - match Contact::add_or_lookup(context, &contact.authname, &addr, origin).await { - Err(e) => return Err(e).context("Contact::add_or_lookup() failed"), - Ok((ContactId::SELF, _)) => return Ok(ContactId::SELF), - Ok(val) => val, - }; - if modified != Modifier::None { - context.emit_event(EventType::ContactsChanged(Some(id))); - } let key = contact.key.as_ref().and_then(|k| { SignedPublicKey::from_base64(k) .with_context(|| { @@ -324,6 +338,23 @@ async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Resu .log_err(context) .ok() }); + let fingerprint = key.as_ref().map(|k| k.dc_fingerprint().hex()); + let (id, modified) = match Contact::add_or_lookup_ex( + context, + &contact.authname, + &addr, + &fingerprint.unwrap_or_default(), + origin, + ) + .await + { + Err(e) => return Err(e).context("Contact::add_or_lookup() failed"), + Ok((ContactId::SELF, _)) => return Ok(ContactId::SELF), + Ok(val) => val, + }; + if modified != Modifier::None { + context.emit_event(EventType::ContactsChanged(Some(id))); + } if let Some(public_key) = key { let timestamp = contact .timestamp @@ -431,6 +462,9 @@ pub struct Contact { /// E-Mail-Address of the contact. It is recommended to use `Contact::get_addr` to access this field. addr: String, + /// OpenPGP fingerprint. + fingerprint: Option, + /// Blocked state. Use contact_is_blocked to access this field. pub blocked: bool, @@ -580,7 +614,7 @@ impl Contact { .sql .query_row_optional( "SELECT c.name, c.addr, c.origin, c.blocked, c.last_seen, - c.authname, c.param, c.status, c.is_bot + c.authname, c.param, c.status, c.is_bot, c.fingerprint FROM contacts c WHERE c.id=?;", (contact_id,), @@ -594,11 +628,14 @@ impl Contact { let param: String = row.get(6)?; let status: Option = row.get(7)?; let is_bot: bool = row.get(8)?; + let fingerprint: Option = + Some(row.get(9)?).filter(|s: &String| !s.is_empty()); let contact = Self { id: contact_id, name, authname, addr, + fingerprint, blocked: blocked.unwrap_or_default(), last_seen, origin, @@ -621,6 +658,9 @@ impl Contact { .get_config(Config::ConfiguredAddr) .await? .unwrap_or_default(); + if let Some(public_key) = load_self_public_key_opt(context).await? { + contact.fingerprint = Some(public_key.dc_fingerprint().hex()); + } contact.status = context .get_config(Config::Selfstatus) .await? @@ -793,6 +833,15 @@ impl Contact { Ok(id) } + pub(crate) async fn add_or_lookup( + context: &Context, + name: &str, + addr: &ContactAddress, + origin: Origin, + ) -> Result<(ContactId, Modifier)> { + Self::add_or_lookup_ex(context, name, addr, "", origin).await + } + /// Lookup a contact and create it if it does not exist yet. /// The contact is identified by the email-address, a name and an "origin" can be given. /// @@ -818,21 +867,32 @@ impl Contact { /// Depending on the origin, both, "row_name" and "row_authname" are updated from "name". /// /// Returns the contact_id and a `Modifier` value indicating if a modification occurred. - pub(crate) async fn add_or_lookup( + pub(crate) async fn add_or_lookup_ex( context: &Context, name: &str, - addr: &ContactAddress, + addr: &str, + fingerprint: &str, mut origin: Origin, ) -> Result<(ContactId, Modifier)> { let mut sth_modified = Modifier::None; - ensure!(!addr.is_empty(), "Can not add_or_lookup empty address"); + ensure!( + !addr.is_empty() || !fingerprint.is_empty(), + "Can not add_or_lookup empty address" + ); ensure!(origin != Origin::Unknown, "Missing valid origin"); if context.is_self_addr(addr).await? { return Ok((ContactId::SELF, sth_modified)); } + if !fingerprint.is_empty() { + let fingerprint_self = load_self_public_key(context).await?.dc_fingerprint().hex(); + if fingerprint == fingerprint_self { + return Ok((ContactId::SELF, sth_modified)); + } + } + let mut name = sanitize_name(name); if origin <= Origin::OutgoingTo { // The user may accidentally have written to a "noreply" address with another MUA: @@ -868,8 +928,10 @@ impl Contact { let row = transaction .query_row( "SELECT id, name, addr, origin, authname - FROM contacts WHERE addr=? COLLATE NOCASE", - (addr,), + FROM contacts + WHERE (?1<>'' AND fingerprint=?1) + OR (?1='' AND addr=?2 COLLATE NOCASE)", + (fingerprint, addr), |row| { let row_id: u32 = row.get(0)?; let row_name: String = row.get(1)?; @@ -893,7 +955,7 @@ impl Contact { || row_authname.is_empty()); row_id = id; - if origin >= row_origin && addr.as_ref() != row_addr { + if origin >= row_origin && addr != row_addr { update_addr = true; } if update_name || update_authname || update_addr || origin > row_origin { @@ -937,11 +999,12 @@ impl Contact { let update_authname = !manual; transaction.execute( - "INSERT INTO contacts (name, addr, origin, authname) - VALUES (?, ?, ?, ?);", + "INSERT INTO contacts (name, addr, fingerprint, origin, authname) + VALUES (?, ?, ?, ?, ?);", ( if update_name { &name } else { "" }, &addr, + fingerprint, origin, if update_authname { &name } else { "" }, ), @@ -949,7 +1012,14 @@ impl Contact { sth_modified = Modifier::Created; row_id = u32::try_from(transaction.last_insert_rowid())?; - info!(context, "Added contact id={row_id} addr={addr}."); + if fingerprint.is_empty() { + info!(context, "Added contact id={row_id} addr={addr}."); + } else { + info!( + context, + "Added contact id={row_id} fpr={fingerprint} addr={addr}." + ); + } } Ok(row_id) }) @@ -1352,6 +1422,19 @@ impl Contact { &self.addr } + /// Returns true if the contact is a PGP-contact. + /// Otherwise it is an email contact. + pub fn is_pgp_contact(&self) -> bool { + self.fingerprint.is_some() + } + + /// Returns OpenPGP fingerprint of a contact. + /// + /// `None` for e-mail contacts. + pub fn fingerprint(&self) -> Option<&str> { + self.fingerprint.as_deref() + } + /// Get name authorized by the contact. pub fn get_authname(&self) -> &str { &self.authname diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs index 1e28b00f62..e7843c16ee 100644 --- a/src/contact/contact_tests.rs +++ b/src/contact/contact_tests.rs @@ -133,7 +133,7 @@ async fn test_is_self_addr() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_add_or_lookup() { // add some contacts, this also tests add_address_book() - let t = TestContext::new().await; + let t = TestContext::new_alice().await; let book = concat!( " Name one \n one@eins.org \n", "Name two\ntwo@deux.net\n", @@ -247,7 +247,7 @@ async fn test_add_or_lookup() { // check SELF let contact = Contact::get_by_id(&t, ContactId::SELF).await.unwrap(); assert_eq!(contact.get_name(), stock_str::self_msg(&t).await); - assert_eq!(contact.get_addr(), ""); // we're not configured + assert_eq!(contact.get_addr(), "alice@example.org"); assert!(!contact.is_blocked()); } @@ -1063,8 +1063,7 @@ async fn test_make_n_import_vcard() -> Result<()> { let bob_addr = bob.get_config(Config::Addr).await?.unwrap(); let chat = bob.create_chat(alice).await; let sent_msg = bob.send_text(chat.id, "moin").await; - alice.recv_msg(&sent_msg).await; - let bob_id = Contact::create(alice, "Some Bob", &bob_addr).await?; + let bob_id = alice.recv_msg(&sent_msg).await.from_id; let key_base64 = Peerstate::from_addr(alice, &bob_addr) .await? .unwrap() @@ -1150,8 +1149,10 @@ async fn test_make_n_import_vcard() -> Result<()> { Ok(()) } +/// Tests importing a vCard with the same email address, +/// but a new key. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_import_vcard_updates_only_key() -> Result<()> { +async fn test_import_vcard_key_change() -> Result<()> { let alice = &TestContext::new_alice().await; let bob = &TestContext::new_bob().await; let bob_addr = &bob.get_config(Config::Addr).await?.unwrap(); @@ -1171,7 +1172,7 @@ async fn test_import_vcard_updates_only_key() -> Result<()> { let bob = &TestContext::new().await; bob.configure_addr(bob_addr).await; - bob.set_config(Config::Displayname, Some("Not Bob")).await?; + bob.set_config(Config::Displayname, Some("New Bob")).await?; let avatar_path = bob.dir.path().join("avatar.png"); let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png"); tokio::fs::write(&avatar_path, avatar_bytes).await?; @@ -1179,10 +1180,14 @@ async fn test_import_vcard_updates_only_key() -> Result<()> { .await?; SystemTime::shift(Duration::from_secs(1)); let vcard1 = make_vcard(bob, &[ContactId::SELF]).await?; - assert_eq!(import_vcard(alice, &vcard1).await?, vec![alice_bob_id]); + let alice_bob_id1 = import_vcard(alice, &vcard1).await?[0]; + assert_ne!(alice_bob_id1, alice_bob_id); let alice_bob_contact = Contact::get_by_id(alice, alice_bob_id).await?; assert_eq!(alice_bob_contact.get_authname(), "Bob"); assert_eq!(alice_bob_contact.get_profile_image(alice).await?, None); + let alice_bob_contact1 = Contact::get_by_id(alice, alice_bob_id1).await?; + assert_eq!(alice_bob_contact1.get_authname(), "New Bob"); + assert!(alice_bob_contact1.get_profile_image(alice).await?.is_some()); let msg = alice.get_last_msg_in(chat_id).await; assert!(msg.is_info()); assert_eq!( @@ -1211,9 +1216,27 @@ async fn test_self_is_verified() -> Result<()> { assert_eq!(contact.is_verified(&alice).await?, true); assert!(contact.is_profile_verified(&alice).await?); assert!(contact.get_verifier_id(&alice).await?.is_none()); + assert!(contact.is_pgp_contact()); let chat_id = ChatId::get_for_contact(&alice, ContactId::SELF).await?; assert!(chat_id.is_protected(&alice).await.unwrap() == ProtectionStatus::Protected); Ok(()) } + +/// Tests that importing a vCard with a key creates a PGP-contact. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_vcard_creates_pgp_contact() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + let vcard = make_vcard(bob, &[ContactId::SELF]).await?; + let contact_ids = import_vcard(alice, &vcard).await?; + assert_eq!(contact_ids.len(), 1); + let contact_id = contact_ids.first().unwrap(); + let contact = Contact::get_by_id(alice, *contact_id).await?; + assert!(contact.is_pgp_contact()); + + Ok(()) +} diff --git a/src/decrypt.rs b/src/decrypt.rs index 7be616c309..a04558210d 100644 --- a/src/decrypt.rs +++ b/src/decrypt.rs @@ -143,19 +143,6 @@ pub(crate) fn validate_detached_signature<'a, 'b>( } } -/// Returns public keyring for `peerstate`. -pub(crate) fn keyring_from_peerstate(peerstate: Option<&Peerstate>) -> Vec { - let mut public_keyring_for_validate = Vec::new(); - if let Some(peerstate) = peerstate { - if let Some(key) = &peerstate.public_key { - public_keyring_for_validate.push(key.clone()); - } else if let Some(key) = &peerstate.gossip_key { - public_keyring_for_validate.push(key.clone()); - } - } - public_keyring_for_validate -} - /// Applies Autocrypt header to Autocrypt peer state and saves it into the database. /// /// If we already know this fingerprint from another contact's peerstate, return that diff --git a/src/events/chatlist_events.rs b/src/events/chatlist_events.rs index 4b612f3eed..57d0968258 100644 --- a/src/events/chatlist_events.rs +++ b/src/events/chatlist_events.rs @@ -247,8 +247,7 @@ mod test_chatlist_events { bob.evtracker.clear_events(); // set name - let addr = alice_on_bob.get_addr(); - Contact::create(&bob, "Alice2", addr).await?; + alice_on_bob.id.set_name(&bob, "Alice2").await?; assert!(bob.add_or_lookup_contact(&alice).await.get_display_name() == "Alice2"); wait_for_chatlist_all_items(&bob).await; diff --git a/src/imap.rs b/src/imap.rs index 40d6a982ed..1dd934ddcf 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -1886,7 +1886,7 @@ async fn should_move_out_of_spam( }; // No chat found. let (from_id, blocked_contact, _origin) = - match from_field_to_contact_id(context, &from, true) + match from_field_to_contact_id(context, &from, None, true) .await .context("from_field_to_contact_id")? { @@ -2244,7 +2244,7 @@ pub(crate) async fn prefetch_should_download( None => return Ok(false), }; let (_from_id, blocked_contact, origin) = - match from_field_to_contact_id(context, &from, true).await? { + match from_field_to_contact_id(context, &from, None, true).await? { Some(res) => res, None => return Ok(false), }; diff --git a/src/key.rs b/src/key.rs index 29a2a5d9d5..5afa11bdf1 100644 --- a/src/key.rs +++ b/src/key.rs @@ -130,8 +130,11 @@ pub(crate) trait DcKey: Serialize + Deserializable + PublicKeyTrait + Clone { fn is_private() -> bool; } -pub(crate) async fn load_self_public_key(context: &Context) -> Result { - let public_key = context +/// Attempts to load own public key. +/// +/// Returns `None` if no key is generated yet. +pub(crate) async fn load_self_public_key_opt(context: &Context) -> Result> { + let Some(public_key_bytes) = context .sql .query_row_optional( "SELECT public_key @@ -143,9 +146,20 @@ pub(crate) async fn load_self_public_key(context: &Context) -> Result SignedPublicKey::from_slice(&bytes), + .await? + else { + return Ok(None); + }; + let public_key = SignedPublicKey::from_slice(&public_key_bytes)?; + Ok(Some(public_key)) +} + +/// Loads own public key. +/// +/// If no key is generated yet, generates a new one. +pub(crate) async fn load_self_public_key(context: &Context) -> Result { + match load_self_public_key_opt(context).await? { + Some(public_key) => Ok(public_key), None => { let keypair = generate_keypair(context).await?; Ok(keypair.public) diff --git a/src/message/message_tests.rs b/src/message/message_tests.rs index 7c4bb07347..c62d6d2584 100644 --- a/src/message/message_tests.rs +++ b/src/message/message_tests.rs @@ -106,7 +106,7 @@ async fn test_create_webrtc_instance_noroom() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_get_width_height() { - let t = TestContext::new().await; + let t = TestContext::new_alice().await; // test that get_width() and get_height() are returning some dimensions for images; // (as the device-chat contains a welcome-images, we check that) diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 980ba2414f..522b0f71e3 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -22,8 +22,7 @@ use crate::constants; use crate::contact::ContactId; use crate::context::Context; use crate::decrypt::{ - get_autocrypt_peerstate, get_encrypted_mime, keyring_from_peerstate, try_decrypt, - validate_detached_signature, + get_autocrypt_peerstate, get_encrypted_mime, try_decrypt, validate_detached_signature, }; use crate::dehtml::dehtml; use crate::events::EventType; @@ -72,16 +71,12 @@ pub(crate) struct MimeMessage { /// `From:` address. pub from: SingleInfo, - /// Whether the From address was repeated in the signed part - /// (and we know that the signer intended to send from this address) - pub from_is_signed: bool, /// Whether the message is incoming or outgoing (self-sent). pub incoming: bool, /// The List-Post address is only set for mailing lists. Users can send /// messages to this address to post them to the list. pub list_post: Option, pub chat_disposition_notification_to: Option, - pub autocrypt_header: Option, pub peerstate: Option, pub decrypting_failed: bool, @@ -331,7 +326,6 @@ impl MimeMessage { let dkim_results = handle_authres(context, &mail, &from.addr).await?; let mut gossiped_keys = Default::default(); - let mut from_is_signed = false; hop_info += "\n\n"; hop_info += &dkim_results.to_string(); @@ -417,9 +411,14 @@ impl MimeMessage { ) .await?; - let public_keyring = match peerstate.is_none() && !incoming { - true => key::load_self_public_keyring(context).await?, - false => keyring_from_peerstate(peerstate.as_ref()), + let public_keyring = if incoming { + if let Some(autocrypt_header) = autocrypt_header { + vec![autocrypt_header.public_key] + } else { + vec![] + } + } else { + key::load_self_public_keyring(context).await? }; let mut signatures = if let Some(ref decrypted_msg) = decrypted_msg { @@ -514,20 +513,10 @@ impl MimeMessage { bail!("From header is forged"); } from = inner_from; - from_is_signed = !signatures.is_empty(); } } if signatures.is_empty() { Self::remove_secured_headers(&mut headers, &mut headers_removed); - - // If it is not a read receipt, degrade encryption. - if let (Some(peerstate), Ok(mail)) = (&mut peerstate, mail) { - if timestamp_sent > peerstate.last_seen_autocrypt - && mail.ctype.mimetype != "multipart/report" - { - peerstate.degrade_encryption(timestamp_sent); - } - } } if !encrypted { signatures.clear(); @@ -549,10 +538,8 @@ impl MimeMessage { past_members, list_post, from, - from_is_signed, incoming, chat_disposition_notification_to, - autocrypt_header, peerstate, decrypting_failed: mail.is_err(), diff --git a/src/peerstate.rs b/src/peerstate.rs index daef7beadb..e2f1840b21 100644 --- a/src/peerstate.rs +++ b/src/peerstate.rs @@ -1,19 +1,14 @@ //! # [Autocrypt Peer State](https://autocrypt.org/level1.html#peer-state-management) module. -use std::mem; - use anyhow::{Context as _, Error, Result}; -use deltachat_contact_tools::{addr_cmp, ContactAddress}; +use deltachat_contact_tools::addr_cmp; use num_traits::FromPrimitive; use crate::aheader::{Aheader, EncryptPreference}; -use crate::chat::{self, Chat}; +use crate::chat; use crate::chatlist::Chatlist; use crate::config::Config; -use crate::constants::Chattype; -use crate::contact::{Contact, Origin}; use crate::context::Context; -use crate::events::EventType; use crate::key::{DcKey, Fingerprint, SignedPublicKey}; use crate::message::Message; use crate::mimeparser::SystemMessage; @@ -638,16 +633,6 @@ impl Peerstate { PeerstateChange::FingerprintChange => { stock_str::contact_setup_changed(context, &self.addr).await } - PeerstateChange::Aeap(new_addr) => { - let old_contact = Contact::get_by_id(context, contact_id).await?; - stock_str::aeap_addr_changed( - context, - old_contact.get_display_name(), - &self.addr, - new_addr, - ) - .await - } }; for (chat_id, msg_id) in chats.iter() { let timestamp_sort = if let Some(msg_id) = msg_id { @@ -657,66 +642,6 @@ impl Peerstate { chat_id.created_timestamp(context).await? }; - if let PeerstateChange::Aeap(new_addr) = &change { - let chat = Chat::load_from_db(context, *chat_id).await?; - - if chat.typ == Chattype::Group && !chat.is_protected() { - // Don't add an info_msg to the group, in order not to make the user think - // that the address was automatically replaced in the group. - continue; - } - - // For security reasons, for now, we only do the AEAP transition if the fingerprint - // is verified (that's what from_verified_fingerprint_or_addr() does). - // In order to not have inconsistent group membership state, we then only do the - // transition in verified groups and in broadcast lists. - if (chat.typ == Chattype::Group && chat.is_protected()) - || chat.typ == Chattype::Broadcast - { - match ContactAddress::new(new_addr) { - Ok(new_addr) => { - let (new_contact_id, _) = Contact::add_or_lookup( - context, - "", - &new_addr, - Origin::IncomingUnknownFrom, - ) - .await?; - context - .sql - .transaction(|transaction| { - transaction.execute( - "UPDATE chats_contacts - SET remove_timestamp=MAX(add_timestamp+1, ?) - WHERE chat_id=? AND contact_id=?", - (timestamp, chat_id, contact_id), - )?; - transaction.execute( - "INSERT INTO chats_contacts - (chat_id, contact_id, add_timestamp) - VALUES (?1, ?2, ?3) - ON CONFLICT (chat_id, contact_id) - DO UPDATE SET add_timestamp=MAX(remove_timestamp, ?3)", - (chat_id, new_contact_id, timestamp), - )?; - Ok(()) - }) - .await?; - - context.emit_event(EventType::ChatModified(*chat_id)); - } - Err(err) => { - warn!( - context, - "New address {:?} is not valid, not doing AEAP: {:#}.", - new_addr, - err - ) - } - } - } - } - chat::add_info_msg_with_cmd( context, *chat_id, @@ -751,105 +676,6 @@ impl Peerstate { } } -/// Do an AEAP transition, if necessary. -/// AEAP stands for "Automatic Email Address Porting." -/// -/// In `drafts/aeap_mvp.md` there is a "big picture" overview over AEAP. -pub(crate) async fn maybe_do_aeap_transition( - context: &Context, - mime_parser: &mut crate::mimeparser::MimeMessage, -) -> Result<()> { - let Some(peerstate) = &mime_parser.peerstate else { - return Ok(()); - }; - - // If the from addr is different from the peerstate address we know, - // we may want to do an AEAP transition. - if !addr_cmp(&peerstate.addr, &mime_parser.from.addr) { - // Check if it's a chat message; we do this to avoid - // some accidental transitions if someone writes from multiple - // addresses with an MUA. - if !mime_parser.has_chat_version() { - info!( - context, - "Not doing AEAP from {} to {} because the message is not a chat message.", - &peerstate.addr, - &mime_parser.from.addr - ); - return Ok(()); - } - - // Check if the message is encrypted and signed correctly. If it's not encrypted, it's - // probably from a new contact sharing the same key. - if mime_parser.signatures.is_empty() { - info!( - context, - "Not doing AEAP from {} to {} because the message is not encrypted and signed.", - &peerstate.addr, - &mime_parser.from.addr - ); - return Ok(()); - } - - // Check if the From: address was also in the signed part of the email. - // Without this check, an attacker could replay a message from Alice - // to Bob. Then Bob's device would do an AEAP transition from Alice's - // to the attacker's address, allowing for easier phishing. - if !mime_parser.from_is_signed { - info!( - context, - "Not doing AEAP from {} to {} because From: is not signed.", - &peerstate.addr, - &mime_parser.from.addr - ); - return Ok(()); - } - - // DC avoids sending messages with the same timestamp, that's why messages - // with equal timestamps are ignored here unlike in `Peerstate::apply_header()`. - if mime_parser.timestamp_sent <= peerstate.last_seen { - info!( - context, - "Not doing AEAP from {} to {} because {} < {}.", - &peerstate.addr, - &mime_parser.from.addr, - mime_parser.timestamp_sent, - peerstate.last_seen - ); - return Ok(()); - } - - info!( - context, - "Doing AEAP transition from {} to {}.", &peerstate.addr, &mime_parser.from.addr - ); - - let peerstate = mime_parser.peerstate.as_mut().context("no peerstate??")?; - // Add info messages to chats with this (verified) contact - // - peerstate - .handle_setup_change( - context, - mime_parser.timestamp_sent, - PeerstateChange::Aeap(mime_parser.from.addr.clone()), - ) - .await?; - - let old_addr = mem::take(&mut peerstate.addr); - peerstate.addr.clone_from(&mime_parser.from.addr); - let header = mime_parser.autocrypt_header.as_ref().context( - "Internal error: Tried to do an AEAP transition without an autocrypt header??", - )?; - peerstate.apply_header(context, header, mime_parser.timestamp_sent); - - peerstate - .save_to_db_ex(&context.sql, Some(&old_addr)) - .await?; - } - - Ok(()) -} - /// Type of the peerstate change. /// /// Changes to the peerstate are notified to the user via a message @@ -858,9 +684,6 @@ enum PeerstateChange { /// The contact's public key fingerprint changed, likely because /// the contact uses a new device and didn't transfer their key. FingerprintChange, - /// The contact changed their address to the given new address - /// (Automatic Email Address Porting). - Aeap(String), } #[cfg(test)] diff --git a/src/qr.rs b/src/qr.rs index edfd62c120..b8ee21d030 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -464,10 +464,15 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result { if let (Some(addr), Some(invitenumber), Some(authcode)) = (&addr, invitenumber, authcode) { let addr = ContactAddress::new(addr)?; - let (contact_id, _) = - Contact::add_or_lookup(context, &name, &addr, Origin::UnhandledSecurejoinQrScan) - .await - .with_context(|| format!("failed to add or lookup contact for address {addr:?}"))?; + let (contact_id, _) = Contact::add_or_lookup_ex( + context, + &name, + &addr, + &fingerprint.hex(), + Origin::UnhandledSecurejoinQrScan, + ) + .await + .with_context(|| format!("failed to add or lookup contact for address {addr:?}"))?; if let (Some(grpid), Some(grpname)) = (grpid, grpname) { if context diff --git a/src/qr/qr_tests.rs b/src/qr/qr_tests.rs index fa507b1bdc..77713e0c81 100644 --- a/src/qr/qr_tests.rs +++ b/src/qr/qr_tests.rs @@ -212,7 +212,7 @@ async fn test_decode_smtp() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_decode_ideltachat_link() -> Result<()> { - let ctx = TestContext::new().await; + let ctx = TestContext::new_alice().await; let qr = check_qr( &ctx.ctx, @@ -233,7 +233,7 @@ async fn test_decode_ideltachat_link() -> Result<()> { // see issue https://github.com/deltachat/deltachat-core-rust/issues/1969 for more info #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_decode_openpgp_tolerance_for_issue_1969() -> Result<()> { - let ctx = TestContext::new().await; + let ctx = TestContext::new_alice().await; let qr = check_qr( &ctx.ctx, @@ -246,7 +246,7 @@ async fn test_decode_openpgp_tolerance_for_issue_1969() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_decode_openpgp_group() -> Result<()> { - let ctx = TestContext::new().await; + let ctx = TestContext::new_alice().await; let qr = check_qr( &ctx.ctx, "OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL" @@ -264,7 +264,7 @@ async fn test_decode_openpgp_group() -> Result<()> { } // Test it again with lowercased "openpgp4fpr:" uri scheme - let ctx = TestContext::new().await; + let ctx = TestContext::new_alice().await; let qr = check_qr( &ctx.ctx, "openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL" @@ -304,7 +304,7 @@ async fn test_decode_openpgp_invalid_token() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_decode_openpgp_secure_join() -> Result<()> { - let ctx = TestContext::new().await; + let ctx = TestContext::new_alice().await; let qr = check_qr( &ctx.ctx, @@ -333,7 +333,7 @@ async fn test_decode_openpgp_secure_join() -> Result<()> { } // Regression test - let ctx = TestContext::new().await; + let ctx = TestContext::new_alice().await; let qr = check_qr( &ctx.ctx, "openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&n=&i=TbnwJ6lSvD5&s=0ejvbdFSQxB" diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 0d96466887..94a700ba42 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -25,6 +25,7 @@ use crate::events::EventType; use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::imap::{markseen_on_imap_table, GENERATED_PREFIX}; use crate::key::DcKey; +use crate::key::Fingerprint; use crate::log::LogExt; use crate::message::{ self, rfc724_mid_exists, Message, MessageState, MessengerMessage, MsgId, Viewtype, @@ -66,11 +67,6 @@ pub struct ReceivedMsg { /// Whether IMAP messages should be immediately deleted. pub needs_delete_job: bool, - - /// Whether the From address was repeated in the signed part - /// (and we know that the signer intended to send from this address). - #[cfg(test)] - pub(crate) from_is_signed: bool, } /// Emulates reception of a message from the network. @@ -196,27 +192,11 @@ pub(crate) async fn receive_imf_inner( sort_timestamp: 0, msg_ids, needs_delete_job: false, - #[cfg(test)] - from_is_signed: false, })); } Ok(mime_parser) => mime_parser, }; - crate::peerstate::maybe_do_aeap_transition(context, &mut mime_parser).await?; - if let Some(peerstate) = &mime_parser.peerstate { - peerstate - .handle_fingerprint_change(context, mime_parser.timestamp_sent) - .await?; - // When peerstate is set to Mutual, it's saved immediately to not lose that fact in case - // of an error. Otherwise we don't save peerstate until get here to reduce the number of - // calls to save_to_db() and not to degrade encryption if a mail wasn't parsed - // successfully. - if peerstate.prefer_encrypt != EncryptPreference::Mutual { - peerstate.save_to_db(&context.sql).await?; - } - } - let rfc724_mid_orig = &mime_parser .get_rfc724_mid() .unwrap_or(rfc724_mid.to_string()); @@ -322,8 +302,11 @@ pub(crate) async fn receive_imf_inner( // For example, GitHub sends messages from `notifications@github.com`, // but uses display name of the user whose action generated the notification // as the display name. + let fingerprint = mime_parser.signatures.iter().next(); let (from_id, _from_id_blocked, incoming_origin) = - match from_field_to_contact_id(context, &mime_parser.from, prevent_rename).await? { + match from_field_to_contact_id(context, &mime_parser.from, fingerprint, prevent_rename) + .await? + { Some(contact_id_res) => contact_id_res, None => { warn!( @@ -346,18 +329,38 @@ pub(crate) async fn receive_imf_inner( }, ) .await?; - let past_ids = add_or_lookup_contacts_by_address_list( - context, - &mime_parser.past_members, - if !mime_parser.incoming { - Origin::OutgoingTo - } else if incoming_origin.is_known() { - Origin::IncomingTo + + let chat_id = if let Some(grpid) = mime_parser.get_chat_group_id() { + if let Some((chat_id, _protected, _blocked)) = + chat::get_chat_id_by_grpid(context, &grpid).await? + { + Some(chat_id) } else { - Origin::IncomingUnknownTo - }, - ) - .await?; + None + } + } else { + None + }; + + let past_ids: Vec> = if let Some(chat_id) = chat_id { + lookup_pgp_contacts_by_address_list(context, &mime_parser.past_members, chat_id).await? + } else { + add_or_lookup_contacts_by_address_list( + context, + &mime_parser.past_members, + if !mime_parser.incoming { + Origin::OutgoingTo + } else if incoming_origin.is_known() { + Origin::IncomingTo + } else { + Origin::IncomingUnknownTo + }, + ) + .await? + .into_iter() + .map(|contact_id| Some(contact_id)) + .collect() + }; update_verified_keys(context, &mut mime_parser, from_id).await?; @@ -390,8 +393,6 @@ pub(crate) async fn receive_imf_inner( sort_timestamp: mime_parser.timestamp_sent, msg_ids: vec![msg_id], needs_delete_job: res == securejoin::HandshakeMessage::Done, - #[cfg(test)] - from_is_signed: mime_parser.from_is_signed, }); } securejoin::HandshakeMessage::Propagate => { @@ -666,8 +667,10 @@ pub(crate) async fn receive_imf_inner( pub async fn from_field_to_contact_id( context: &Context, from: &SingleInfo, + fingerprint: Option<&Fingerprint>, prevent_rename: bool, ) -> Result> { + let fingerprint = fingerprint.as_ref().map(|fp| fp.hex()).unwrap_or_default(); let display_name = if prevent_rename { Some("") } else { @@ -684,10 +687,11 @@ pub async fn from_field_to_contact_id( } }; - let (from_id, _) = Contact::add_or_lookup( + let (from_id, _) = Contact::add_or_lookup_ex( context, display_name.unwrap_or_default(), &from_addr, + &fingerprint, Origin::IncomingUnknownFrom, ) .await?; @@ -711,7 +715,7 @@ async fn add_parts( mime_parser: &mut MimeMessage, imf_raw: &[u8], to_ids: &[ContactId], - past_ids: &[ContactId], + past_ids: &[Option], rfc724_mid: &str, from_id: ContactId, seen: bool, @@ -1749,8 +1753,6 @@ RETURNING id sort_timestamp, msg_ids: created_db_entries, needs_delete_job, - #[cfg(test)] - from_is_signed: mime_parser.from_is_signed, }) } @@ -2117,7 +2119,7 @@ async fn create_group( create_blocked: Blocked, from_id: ContactId, to_ids: &[ContactId], - past_ids: &[ContactId], + past_ids: &[Option], verified_encryption: &VerifiedEncryption, grpid: &str, ) -> Result> { @@ -2252,7 +2254,7 @@ async fn update_chats_contacts_timestamps( chat_id: ChatId, ignored_id: Option, to_ids: &[ContactId], - past_ids: &[ContactId], + past_ids: &[Option], chat_group_member_timestamps: &[i64], ) -> Result { let expected_timestamps_count = to_ids.len() + past_ids.len(); @@ -2347,7 +2349,7 @@ async fn apply_group_changes( chat_id: ChatId, from_id: ContactId, to_ids: &[ContactId], - past_ids: &[ContactId], + past_ids: &[Option], verified_encryption: &VerifiedEncryption, ) -> Result { if chat_id.is_special() { @@ -3247,5 +3249,49 @@ async fn add_or_lookup_contacts_by_address_list( Ok(contact_ids) } +/// Looks up PGP-contacts by email addresses. +/// +/// This is used as a fallback when email addresses are available, +/// but not the fingerprints, e.g. when core 1.157.3 +/// client sends the `To` and `Chat-Group-Past-Members` header +/// but not the corresponding fingerprint list. +/// +/// Lookup is restricted to the chat ID. +/// +/// If contact cannot be found, `None` is returned. +/// This ensures that the length of the result vector +/// is the same as the number of addresses in the header +/// and it is possible to find corresponding +/// `Chat-Group-Member-Timestamps` items. +async fn lookup_pgp_contacts_by_address_list( + context: &Context, + address_list: &[SingleInfo], + chat_id: ChatId, +) -> Result>> { + let mut contact_ids = Vec::new(); + for info in address_list { + let addr = &info.addr; + + let contact_id = context + .sql + .query_row_optional( + "SELECT id FROM contacts + WHERE contacts.addr=? + AND EXISTS (SELECT 1 FROM chats_contacts + WHERE contact_id=contacts.id + AND chat_id=?)", + (addr, chat_id), + |row| { + let contact_id: ContactId = row.get(0)?; + Ok(contact_id) + }, + ) + .await?; + contact_ids.push(contact_id); + } + debug_assert_eq!(address_list.len(), contact_ids.len()); + Ok(contact_ids) +} + #[cfg(test)] mod receive_imf_tests; diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index c9093eb856..72cfc2fc81 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -3636,7 +3636,6 @@ async fn test_thunderbird_autocrypt() -> Result<()> { let raw = include_bytes!("../../test-data/message/thunderbird_with_autocrypt.eml"); let received_msg = receive_imf(&t, raw, false).await?.unwrap(); - assert!(received_msg.from_is_signed); let peerstate = Peerstate::from_addr(&t, "alice@example.org") .await? @@ -3691,7 +3690,6 @@ async fn test_forged_from_and_no_valid_signatures() -> Result<()> { let t = &TestContext::new_bob().await; let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_signed.eml"); let received_msg = receive_imf(t, raw, false).await?.unwrap(); - assert!(!received_msg.from_is_signed); let msg = t.get_last_msg().await; assert!(!msg.chat_id.is_trash()); assert!(!msg.get_showpadlock()); @@ -3710,7 +3708,6 @@ async fn test_wrong_from_name_and_no_valid_signatures() -> Result<()> { let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_signed.eml"); let raw = String::from_utf8(raw.to_vec())?.replace("From: Alice", "From: A"); let received_msg = receive_imf(t, raw.as_bytes(), false).await?.unwrap(); - assert!(!received_msg.from_is_signed); let msg = t.get_last_msg().await; assert!(!msg.chat_id.is_trash()); assert!(!msg.get_showpadlock()); @@ -3751,7 +3748,6 @@ async fn test_thunderbird_unsigned() -> Result<()> { // Alice receives an unsigned message from Bob. let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_unsigned.eml"); let received_msg = receive_imf(&alice, raw, false).await?.unwrap(); - assert!(!received_msg.from_is_signed); let msg = alice.get_last_msg().await; assert!(!msg.get_showpadlock()); diff --git a/src/securejoin.rs b/src/securejoin.rs index e4cad2e4e9..8f81317b06 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -412,6 +412,7 @@ pub(crate) async fn handle_securejoin_handshake( .await? .get_addr() .to_owned(); + let backward_verified = true; let fingerprint_found = mark_peer_as_verified( context, diff --git a/src/securejoin/securejoin_tests.rs b/src/securejoin/securejoin_tests.rs index 663341fe16..3abc2b769c 100644 --- a/src/securejoin/securejoin_tests.rs +++ b/src/securejoin/securejoin_tests.rs @@ -141,7 +141,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) { msg.get_header(HeaderDef::SecureJoin).unwrap(), "vc-auth-required" ); - let bob_chat = bob.get_chat(&alice).await; + let bob_chat = bob.get_pgp_chat(&alice).await; assert_eq!(bob_chat.can_send(&bob).await.unwrap(), false); assert_eq!( bob_chat.why_cant_send(&bob).await.unwrap(), @@ -154,7 +154,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) { // Step 4: Bob receives vc-auth-required, sends vc-request-with-auth bob.recv_msg_trash(&sent).await; - let bob_chat = bob.get_chat(&alice).await; + let bob_chat = bob.get_pgp_chat(&alice).await; assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true); // Check Bob emitted the JoinerProgress event. @@ -227,13 +227,9 @@ async fn test_setup_contact_ex(case: SetupContactCase) { } // Alice should not yet have Bob verified - let contact_bob_id = Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown) - .await - .expect("Error looking up contact") - .expect("Contact not found"); - let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id) - .await - .unwrap(); + let contact_bob = alice.add_or_lookup_pgp_contact(&bob).await; + let contact_bob_id = contact_bob.id; + assert_eq!(contact_bob.is_pgp_contact(), true); assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), false); assert_eq!(contact_bob.get_authname(), ""); @@ -252,7 +248,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) { assert_eq!(contact_bob.is_bot(), false); // exactly one one-to-one chat should be visible for both now - // (check this before calling alice.get_chat() explicitly below) + // (check this before calling alice.get_pgp_chat() explicitly below) assert_eq!( Chatlist::try_load(&alice, 0, None, None) .await @@ -267,7 +263,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) { // Check Alice got the verified message in her 1:1 chat. { - let chat = alice.get_chat(&bob).await; + let chat = alice.get_pgp_chat(&bob).await; let msg = get_chat_msg(&alice, chat.get_id(), 0, 1).await; assert!(msg.is_info()); let expected_text = chat_protection_enabled(&alice).await; @@ -614,7 +610,7 @@ async fn test_secure_join() -> Result<()> { // Now Alice's chat with Bob should still be hidden, the verified message should // appear in the group chat. - let chat = alice.get_chat(&bob).await; + let chat = alice.get_pgp_chat(&bob).await; assert_eq!( chat.blocked, Blocked::Yes, @@ -643,7 +639,7 @@ async fn test_secure_join() -> Result<()> { { // Bob has Alice verified, message shows up in the group chat. assert_eq!(contact_alice.is_verified(&bob.ctx).await?, true); - let chat = bob.get_chat(&alice).await; + let chat = bob.get_pgp_chat(&alice).await; assert_eq!( chat.blocked, Blocked::Yes, diff --git a/src/sql.rs b/src/sql.rs index 1dde1415c0..0ecd6e85c0 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -22,7 +22,6 @@ use crate::net::dns::prune_dns_cache; use crate::net::http::http_cache_cleanup; use crate::net::prune_connection_history; use crate::param::{Param, Params}; -use crate::peerstate::Peerstate; use crate::stock_str; use crate::tools::{delete_file, time, SystemTime}; @@ -202,36 +201,13 @@ impl Sql { // this should be done before updates that use high-level objects that // rely themselves on the low-level structure. - let (recalc_fingerprints, update_icons, disable_server_delete, recode_avatar) = - migrations::run(context, self) - .await - .context("failed to run migrations")?; + let (update_icons, disable_server_delete, recode_avatar) = migrations::run(context, self) + .await + .context("failed to run migrations")?; // (2) updates that require high-level objects // the structure is complete now and all objects are usable - if recalc_fingerprints { - info!(context, "[migration] recalc fingerprints"); - let addrs = self - .query_map( - "SELECT addr FROM acpeerstates;", - (), - |row| row.get::<_, String>(0), - |addrs| { - addrs - .collect::, _>>() - .map_err(Into::into) - }, - ) - .await?; - for addr in &addrs { - if let Some(ref mut peerstate) = Peerstate::from_addr(context, addr).await? { - peerstate.recalc_fingerprint(); - peerstate.save_to_db(self).await?; - } - } - } - if update_icons { update_saved_messages_icon(context).await?; update_device_icon(context).await?; diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index be093db42f..85d1c389c4 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -19,8 +19,7 @@ const DBVERSION: i32 = 68; const VERSION_CFG: &str = "dbversion"; const TABLES: &str = include_str!("./tables.sql"); -pub async fn run(context: &Context, sql: &Sql) -> Result<(bool, bool, bool, bool)> { - let mut recalc_fingerprints = false; +pub async fn run(context: &Context, sql: &Sql) -> Result<(bool, bool, bool)> { let mut exists_before_update = false; let mut dbversion_before_update = DBVERSION; @@ -158,7 +157,6 @@ CREATE INDEX acpeerstates_index4 ON acpeerstates (gossip_key_fingerprint);"#, 34, ) .await?; - recalc_fingerprints = true; } if dbversion < 39 { sql.execute_migration( @@ -1224,6 +1222,39 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint); .await?; } + inc_and_check(&mut migration_version, 132)?; + if dbversion < migration_version { + sql.execute_migration( + "ALTER TABLE contacts ADD COLUMN fingerprint TEXT NOT NULL DEFAULT ''; + CREATE INDEX contacts_fingerprint_index ON contacts (fingerprint); + + CREATE TABLE public_keys ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fingerprint TEXT NOT NULL UNIQUE, -- Upper-case fingerprint of the key. + public_key BLOB NOT NULL -- Binary key, not ASCII-armored + ) STRICT; + CREATE INDEX public_key_index ON public_keys (fingerprint); + + INSERT INTO public_keys (fingerprint, public_key) + SELECT public_key_fingerprint, public_key FROM acpeerstates; + INSERT OR IGNORE INTO public_keys (fingerprint, public_key) + SELECT gossip_key_fingerprint, gossip_key FROM acpeerstates; + INSERT OR IGNORE INTO public_keys (fingerprint, public_key) + SELECT verified_key_fingerprint, verified_key FROM acpeerstates; + INSERT OR IGNORE INTO public_keys (fingerprint, public_key) + SELECT secondary_verified_key_fingerprint, secondary_verified_key FROM acpeerstates; + + CREATE TABLE pgp_contacts_addr ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + contact_id INTEGER NOT NULL UNIQUE, -- ID of a PGP-contact. + addr TEXT NOT NULL -- Email address. + ) STRICT; + ", + migration_version, + ) + .await?; + } + let new_version = sql .get_raw_config_int(VERSION_CFG) .await? @@ -1238,12 +1269,7 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint); } info!(context, "Database version: v{new_version}."); - Ok(( - recalc_fingerprints, - update_icons, - disable_server_delete, - recode_avatar, - )) + Ok((update_icons, disable_server_delete, recode_avatar)) } impl Sql { diff --git a/src/sql/sql_tests.rs b/src/sql/sql_tests.rs index 265c9acf16..c99f29c01e 100644 --- a/src/sql/sql_tests.rs +++ b/src/sql/sql_tests.rs @@ -179,9 +179,7 @@ async fn test_migration_flags() -> Result<()> { // as migrations::run() was already executed on context creation, // another call should not result in any action needed. // this test catches some bugs where dbversion was forgotten to be persisted. - let (recalc_fingerprints, update_icons, disable_server_delete, recode_avatar) = - migrations::run(&t, &t.sql).await?; - assert!(!recalc_fingerprints); + let (update_icons, disable_server_delete, recode_avatar) = migrations::run(&t, &t.sql).await?; assert!(!update_icons); assert!(!disable_server_delete); assert!(!recode_avatar); diff --git a/src/stock_str/stock_str_tests.rs b/src/stock_str/stock_str_tests.rs index 9b3fa018c5..334927b1af 100644 --- a/src/stock_str/stock_str_tests.rs +++ b/src/stock_str/stock_str_tests.rs @@ -133,7 +133,7 @@ async fn test_partial_download_msg_body() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_update_device_chats() { - let t = TestContext::new().await; + let t = TestContext::new_alice().await; t.update_device_chats().await.ok(); let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 2); diff --git a/src/test_utils.rs b/src/test_utils.rs index 8ee9a1c74f..85cf61c7aa 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -32,7 +32,7 @@ use crate::contact::{import_vcard, make_vcard, Contact, ContactId, Modifier, Ori use crate::context::Context; use crate::e2ee::EncryptHelper; use crate::events::{Event, EventEmitter, EventType, Events}; -use crate::key::{self, DcKey, DcSecretKey}; +use crate::key::{self, load_self_public_key, DcKey, DcSecretKey}; use crate::message::{update_msg_state, Message, MessageState, MsgId, Viewtype}; use crate::mimeparser::{MimeMessage, SystemMessage}; use crate::peerstate::Peerstate; @@ -754,12 +754,38 @@ impl TestContext { } /// Returns the [`Contact`] for the other [`TestContext`], creating it if necessary. + /// + /// This function imports a vCard, so will transfer the public key + /// as a side effect. pub async fn add_or_lookup_contact(&self, other: &TestContext) -> Contact { let contact_id = self.add_or_lookup_contact_id(other).await; Contact::get_by_id(&self.ctx, contact_id).await.unwrap() } - /// Returns 1:1 [`Chat`] with another account. Panics if it doesn't exist. + /// Returns the [`Contact`] for the other [`TestContext`], creating it if necessary. + /// + /// If the contact does not exist yet, a new contact will be created + /// with the correct fingerprint, but without the public key. + pub async fn add_or_lookup_pgp_contact(&self, other: &TestContext) -> Contact { + let primary_self_addr = other.ctx.get_primary_self_addr().await.unwrap(); + let addr = ContactAddress::new(&primary_self_addr).unwrap(); + let public_key = load_self_public_key(other).await.unwrap(); + let fingerprint = public_key.dc_fingerprint(); + + let (contact_id, _modified) = Contact::add_or_lookup_ex( + self, + "", + &addr, + &fingerprint.hex(), + Origin::MailinglistAddress, + ) + .await + .expect("add_or_lookup"); + Contact::get_by_id(&self.ctx, contact_id).await.unwrap() + } + + /// Returns 1:1 [`Chat`] with another account email contact. + /// Panics if it doesn't exist. /// May return a blocked chat. /// /// This first creates a contact using the configured details on the other account, then @@ -779,17 +805,35 @@ impl TestContext { Chat::load_from_db(&self.ctx, chat_id).await.unwrap() } + /// Returns 1:1 [`Chat`] with another account PGP-contact. + /// Panics if the chat does not exist. + /// + /// This first creates a contact, but does not import the key, + /// so may create a PGP-contact with a fingerprint + /// but without the key. + pub async fn get_pgp_chat(&self, other: &TestContext) -> Chat { + let contact = self.add_or_lookup_contact_id(other).await; + + let chat_id = ChatIdBlocked::lookup_by_contact(&self.ctx, contact) + .await + .unwrap() + .map(|chat_id_blocked| chat_id_blocked.id) + .expect( + "There is no chat with this contact. \ + Hint: Use create_chat() instead of get_pgp_chat() if this is expected.", + ); + + Chat::load_from_db(&self.ctx, chat_id).await.unwrap() + } + /// Creates or returns an existing 1:1 [`Chat`] with another account. /// /// This first creates a contact by exporting a vCard from the `other` /// and importing it into `self`, /// then creates a 1:1 chat with this contact. pub async fn create_chat(&self, other: &TestContext) -> Chat { - let vcard = make_vcard(other, &[ContactId::SELF]).await.unwrap(); - let contact_ids = import_vcard(self, &vcard).await.unwrap(); - assert_eq!(contact_ids.len(), 1); - let contact_id = contact_ids.first().unwrap(); - let chat_id = ChatId::create_for_contact(self, *contact_id).await.unwrap(); + let contact_id = self.add_or_lookup_contact_id(other).await; + let chat_id = ChatId::create_for_contact(self, contact_id).await.unwrap(); Chat::load_from_db(self, chat_id).await.unwrap() } @@ -913,7 +957,11 @@ impl TestContext { "device-talk".to_string() } else if sel_chat.get_type() == Chattype::Single && !members.is_empty() { let contact = Contact::get_by_id(self, members[0]).await.unwrap(); - contact.get_addr().to_string() + if contact.is_pgp_contact() { + format!("PGP {}", contact.get_addr()) + } else { + contact.get_addr().to_string() + } } else if sel_chat.get_type() == Chattype::Mailinglist && !members.is_empty() { "mailinglist".to_string() } else { diff --git a/src/tests/verified_chats.rs b/src/tests/verified_chats.rs index 0f8dd2794a..aeb46744c8 100644 --- a/src/tests/verified_chats.rs +++ b/src/tests/verified_chats.rs @@ -18,16 +18,16 @@ use crate::tools::SystemTime; use crate::{e2ee, message}; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_verified_oneonone_chat_broken_by_classical() { - check_verified_oneonone_chat(true).await; +async fn test_verified_oneonone_chat_not_broken_by_classical() { + check_verified_oneonone_chat_protection_not_broken(true).await; } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_verified_oneonone_chat_broken_by_device_change() { - check_verified_oneonone_chat(false).await; +async fn test_verified_oneonone_chat_not_broken_by_device_change() { + check_verified_oneonone_chat_protection_not_broken(false).await; } -async fn check_verified_oneonone_chat(broken_by_classical_email: bool) { +async fn check_verified_oneonone_chat_protection_not_broken(broken_by_classical_email: bool) { let mut tcm = TestContextManager::new(); let alice = tcm.alice().await; let bob = tcm.bob().await; @@ -57,7 +57,7 @@ async fn check_verified_oneonone_chat(broken_by_classical_email: bool) { // Bob's contact is still verified, but the chat isn't marked as protected anymore let contact = alice.add_or_lookup_contact(&bob).await; assert_eq!(contact.is_verified(&alice).await.unwrap(), true); - assert_verified(&alice, &bob, ProtectionStatus::ProtectionBroken).await; + assert_verified(&alice, &bob, ProtectionStatus::Protected).await; } else { tcm.section("Bob sets up another Delta Chat device"); let bob2 = TestContext::new().await; @@ -69,7 +69,7 @@ async fn check_verified_oneonone_chat(broken_by_classical_email: bool) { .await; let contact = alice.add_or_lookup_contact(&bob).await; assert_eq!(contact.is_verified(&alice).await.unwrap(), false); - assert_verified(&alice, &bob, ProtectionStatus::ProtectionBroken).await; + assert_verified(&alice, &bob, ProtectionStatus::Protected).await; } tcm.section("Bob sends another message from DC"); @@ -126,7 +126,7 @@ async fn test_create_verified_oneonone_chat() -> Result<()> { // Alice should have a hidden protected chat with Fiona { - let chat = alice.get_chat(&fiona).await; + let chat = alice.get_pgp_chat(&fiona).await; assert!(chat.is_protected()); let msg = get_chat_msg(&alice, chat.id, 0, 1).await; @@ -136,7 +136,7 @@ async fn test_create_verified_oneonone_chat() -> Result<()> { // Fiona should have a hidden protected chat with Alice { - let chat = fiona.get_chat(&alice).await; + let chat = fiona.get_pgp_chat(&alice).await; assert!(chat.is_protected()); let msg0 = get_chat_msg(&fiona, chat.id, 0, 1).await; @@ -1027,7 +1027,7 @@ async fn assert_verified(this: &TestContext, other: &TestContext, protected: Pro assert_eq!(contact.is_verified(this).await.unwrap(), true); } - let chat = this.get_chat(other).await; + let chat = this.get_pgp_chat(other).await; let (expect_protected, expect_broken) = match protected { ProtectionStatus::Unprotected => (false, false), ProtectionStatus::Protected => (true, false), diff --git a/test-data/golden/test_old_message_1 b/test-data/golden/test_old_message_1 index 61e54d2989..d18664dafb 100644 --- a/test-data/golden/test_old_message_1 +++ b/test-data/golden/test_old_message_1 @@ -1,4 +1,4 @@ -Single#Chat#10: Bob [bob@example.net] +Single#Chat#10: Bob [PGP bob@example.net] -------------------------------------------------------------------------------- Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️] Msg#11: info (Contact#Contact#Info): Bob sent a message from another device. [NOTICED][INFO 🛡️❌] diff --git a/test-data/golden/test_old_message_2 b/test-data/golden/test_old_message_2 index d7e7712abe..842ddc8b22 100644 --- a/test-data/golden/test_old_message_2 +++ b/test-data/golden/test_old_message_2 @@ -1,4 +1,4 @@ -Single#Chat#10: Bob [bob@example.net] +Single#Chat#10: Bob [PGP bob@example.net] -------------------------------------------------------------------------------- Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️] Msg#11: info (Contact#Contact#Info): Bob sent a message from another device. [NOTICED][INFO 🛡️❌] diff --git a/test-data/golden/test_old_message_3 b/test-data/golden/test_old_message_3 index 82630528aa..858c3e5250 100644 --- a/test-data/golden/test_old_message_3 +++ b/test-data/golden/test_old_message_3 @@ -1,4 +1,4 @@ -Single#Chat#10: Bob [bob@example.net] 🛡️ +Single#Chat#10: Bob [PGP bob@example.net] 🛡️ -------------------------------------------------------------------------------- Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️] Msg#11🔒: (Contact#Contact#10): Heyho from my verified device! [FRESH] diff --git a/test-data/golden/test_outgoing_mua_msg b/test-data/golden/test_outgoing_mua_msg index 549f84175c..aa2bac6201 100644 --- a/test-data/golden/test_outgoing_mua_msg +++ b/test-data/golden/test_outgoing_mua_msg @@ -1,4 +1,4 @@ -Single#Chat#10: bob@example.net [bob@example.net] 🛡️ +Single#Chat#10: bob@example.net [PGP bob@example.net] 🛡️ -------------------------------------------------------------------------------- Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️] Msg#11🔒: (Contact#Contact#10): Heyho from DC [FRESH] diff --git a/test-data/golden/test_verified_oneonone_chat_enable_disable b/test-data/golden/test_verified_oneonone_chat_enable_disable index 4da4a67d01..df3a1f707c 100644 --- a/test-data/golden/test_verified_oneonone_chat_enable_disable +++ b/test-data/golden/test_verified_oneonone_chat_enable_disable @@ -1,4 +1,4 @@ -Single#Chat#10: Bob [bob@example.net] 🛡️ +Single#Chat#10: Bob [PGP bob@example.net] 🛡️ -------------------------------------------------------------------------------- Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️] Msg#11: info (Contact#Contact#Info): Bob sent a message from another device. [NOTICED][INFO 🛡️❌] diff --git a/test-data/golden/verified_chats_message_from_old_dc_setup b/test-data/golden/verified_chats_message_from_old_dc_setup index 090fcae1df..b319750902 100644 --- a/test-data/golden/verified_chats_message_from_old_dc_setup +++ b/test-data/golden/verified_chats_message_from_old_dc_setup @@ -1,4 +1,4 @@ -Single#Chat#10: bob@example.net [bob@example.net] 🛡️ +Single#Chat#10: bob@example.net [PGP bob@example.net] 🛡️ -------------------------------------------------------------------------------- Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️] Msg#11🔒: (Contact#Contact#10): Now i have it! [FRESH] From cc12f66928f0fdb136d9ad393db54a107187defd Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 8 Apr 2025 14:53:27 +0000 Subject: [PATCH 003/381] remove allow_aeap --- src/decrypt.rs | 20 +------------------- src/mimeparser.rs | 3 --- 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/src/decrypt.rs b/src/decrypt.rs index a04558210d..3774f97437 100644 --- a/src/decrypt.rs +++ b/src/decrypt.rs @@ -145,37 +145,19 @@ pub(crate) fn validate_detached_signature<'a, 'b>( /// Applies Autocrypt header to Autocrypt peer state and saves it into the database. /// -/// If we already know this fingerprint from another contact's peerstate, return that -/// peerstate in order to make AEAP work, but don't save it into the db yet. -/// /// Returns updated peerstate. pub(crate) async fn get_autocrypt_peerstate( context: &Context, from: &str, autocrypt_header: Option<&Aheader>, message_time: i64, - allow_aeap: bool, ) -> Result> { let allow_change = !context.is_self_addr(from).await?; let mut peerstate; // Apply Autocrypt header if let Some(header) = autocrypt_header { - if allow_aeap { - // If we know this fingerprint from another addr, - // we may want to do a transition from this other addr - // (and keep its peerstate) - // For security reasons, for now, we only do a transition - // if the fingerprint is verified. - peerstate = Peerstate::from_verified_fingerprint_or_addr( - context, - &header.public_key.dc_fingerprint(), - from, - ) - .await?; - } else { - peerstate = Peerstate::from_addr(context, from).await?; - } + peerstate = Peerstate::from_addr(context, from).await?; if let Some(ref mut peerstate) = peerstate { if addr_cmp(&peerstate.addr, from) { diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 522b0f71e3..e581546429 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -321,8 +321,6 @@ impl MimeMessage { let mut from = from.context("No from in message")?; let private_keyring = load_self_secret_keyring(context).await?; - let allow_aeap = get_encrypted_mime(&mail).is_some(); - let dkim_results = handle_authres(context, &mail, &from.addr).await?; let mut gossiped_keys = Default::default(); @@ -407,7 +405,6 @@ impl MimeMessage { &from.addr, autocrypt_header.as_ref(), timestamp_sent, - allow_aeap, ) .await?; From 2f1ebc5f212b612957c768292f2d3ddff47a6ab0 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 8 Apr 2025 15:03:41 +0000 Subject: [PATCH 004/381] TODO: mark as verified --- src/receive_imf.rs | 60 +--------------------------------------------- 1 file changed, 1 insertion(+), 59 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 94a700ba42..9ef8c6196f 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -3111,65 +3111,7 @@ async fn mark_recipients_as_verified( continue; } - let Some((to_addr, is_verified)) = context - .sql - .query_row_optional( - "SELECT c.addr, LENGTH(ps.verified_key_fingerprint) FROM contacts c - LEFT JOIN acpeerstates ps ON c.addr=ps.addr WHERE c.id=?", - (id,), - |row| { - let to_addr: String = row.get(0)?; - let is_verified: i32 = row.get(1).unwrap_or(0); - Ok((to_addr, is_verified != 0)) - }, - ) - .await? - else { - continue; - }; - // mark gossiped keys (if any) as verified - if let Some(gossiped_key) = mimeparser.gossiped_keys.get(&to_addr.to_lowercase()) { - if let Some(mut peerstate) = Peerstate::from_addr(context, &to_addr).await? { - // If we're here, we know the gossip key is verified. - // - // Use the gossip-key as verified-key if there is no verified-key. - // - // Store gossip key as secondary verified key if there is a verified key and - // gossiped key is different. - // - // See - // and for discussion. - let verifier_addr = contact.get_addr().to_owned(); - if !is_verified { - info!(context, "{verifier_addr} has verified {to_addr}."); - if let Some(fp) = peerstate.gossip_key_fingerprint.clone() { - peerstate.set_verified(gossiped_key.clone(), fp, verifier_addr)?; - peerstate.backward_verified_key_id = - Some(context.get_config_i64(Config::KeyId).await?).filter(|&id| id > 0); - peerstate.save_to_db(&context.sql).await?; - - let (to_contact_id, _) = Contact::add_or_lookup( - context, - "", - &ContactAddress::new(&to_addr)?, - Origin::Hidden, - ) - .await?; - ChatId::set_protection_for_contact( - context, - to_contact_id, - mimeparser.timestamp_sent, - ) - .await?; - } - } else { - // The contact already has a verified key. - // Store gossiped key as the secondary verified key. - peerstate.set_secondary_verified_key(gossiped_key.clone(), verifier_addr); - peerstate.save_to_db(&context.sql).await?; - } - } - } + // TODO: mark recipient PGP-contacts as verified. } Ok(()) From b987669f83f083a2a5de4395f351ca6c2e7d44ec Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 10 Apr 2025 14:06:24 +0000 Subject: [PATCH 005/381] Encrypt all messages to chats with a group ID --- src/mimefactory.rs | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index af3596c052..7631009912 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -15,7 +15,7 @@ use tokio::fs; use crate::aheader::{Aheader, EncryptPreference}; use crate::blob::BlobObject; -use crate::chat::{self, Chat}; +use crate::chat::{self, Chat, get_chat_contacts}; use crate::config::Config; use crate::constants::ASM_SUBJECT; use crate::constants::{Chattype, DC_FROM_HANDSHAKE}; @@ -753,9 +753,33 @@ impl MimeFactory { } } + let is_chatmail = context.is_chatmail().await?; let peerstates = self.peerstates_for_recipients(context).await?; let is_encrypted = !self.should_force_plaintext() - && (e2ee_guaranteed || encrypt_helper.should_encrypt(context, &peerstates).await?); + && (e2ee_guaranteed + || is_chatmail + || match &self.loaded { + Loaded::Message { chat, .. } => { + match chat.typ { + Chattype::Single => { + let chat_contact_ids = get_chat_contacts(context, chat.id).await?; + if let Some(contact_id) = chat_contact_ids.first() { + let contact = Contact::get_by_id(context, *contact_id).await?; + contact.is_pgp_contact() + } else { + true + } + } + Chattype::Group => { + // Do not encrypt ad-hoc groups. + !chat.grpid.is_empty() + } + Chattype::Mailinglist => false, + Chattype::Broadcast => true, + } + } + Loaded::Mdn { .. } => true, + }); let is_securejoin_message = if let Loaded::Message { msg, .. } = &self.loaded { msg.param.get_cmd() == SystemMessage::SecurejoinMessage } else { From 53e96fa39ec874b5e33da01eb96ab035fe449818 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 10 Apr 2025 16:13:44 +0000 Subject: [PATCH 006/381] test fixup --- src/securejoin/securejoin_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/securejoin/securejoin_tests.rs b/src/securejoin/securejoin_tests.rs index 3abc2b769c..ffb3f54c83 100644 --- a/src/securejoin/securejoin_tests.rs +++ b/src/securejoin/securejoin_tests.rs @@ -279,7 +279,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) { .unwrap(); match case { SetupContactCase::AliceHasName => assert_eq!(contact_alice.get_authname(), "Alice"), - _ => assert_eq!(contact_alice.get_authname(), ""), + _ => assert_eq!(contact_alice.get_authname(), "Alice Exampleorg"), }; // Check Alice sent the right message to Bob. From b8a58ae51a668790e4b91b880fb7ff2f65302dd5 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 10 Apr 2025 16:36:30 +0000 Subject: [PATCH 007/381] test: test receiving member added --- src/chat/chat_tests.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index c08e877947..b4e60bbd1b 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -325,13 +325,15 @@ async fn test_member_add_remove() -> Result<()> { create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?; let alice_fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await; add_contact_to_chat(&alice, alice_chat_id, alice_fiona_contact_id).await?; - alice + let sent = alice .send_text(alice_chat_id, "Hi! I created a group.") .await; + let fiona_chat_id = fiona.recv_msg(&sent).await.chat_id; // Alice adds Bob to the chat. add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?; let sent = alice.pop_sent_msg().await; + fiona.recv_msg(&sent).await; // Locally set name "robert" should not leak. assert!(!sent.payload.contains("robert")); @@ -339,6 +341,12 @@ async fn test_member_add_remove() -> Result<()> { sent.load_from_db().await.get_text(), "You added member robert." ); + let fiona_contact_ids = get_chat_contacts(&fiona, fiona_chat_id).await?; + assert_eq!(fiona_contact_ids.len(), 3); + for contact_id in fiona_contact_ids { + let contact = Contact::get_by_id(&fiona, contact_id).await?; + assert_ne!(contact.get_name(), "robert"); + } // Alice removes Bob from the chat. remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?; From 179d7edb92498e89d7371135ed9f403d82f28af2 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 10 Apr 2025 16:57:51 +0000 Subject: [PATCH 008/381] test: make sure adding contact results in a PGP contact if there is gossip --- src/chat/chat_tests.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index b4e60bbd1b..98089117ec 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -346,6 +346,7 @@ async fn test_member_add_remove() -> Result<()> { for contact_id in fiona_contact_ids { let contact = Contact::get_by_id(&fiona, contact_id).await?; assert_ne!(contact.get_name(), "robert"); + assert!(contact.is_pgp_contact()); } // Alice removes Bob from the chat. From 5e57987300f5369e41d1af76c971a90f92369288 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 10 Apr 2025 17:11:52 +0000 Subject: [PATCH 009/381] WIP: to_ids from gossip --- src/mimefactory.rs | 2 +- src/receive_imf.rs | 105 ++++++++++++++++++++++++++++++++++----------- 2 files changed, 80 insertions(+), 27 deletions(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 7631009912..eae2593ea1 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -15,7 +15,7 @@ use tokio::fs; use crate::aheader::{Aheader, EncryptPreference}; use crate::blob::BlobObject; -use crate::chat::{self, Chat, get_chat_contacts}; +use crate::chat::{self, get_chat_contacts, Chat}; use crate::config::Config; use crate::constants::ASM_SUBJECT; use crate::constants::{Chattype, DC_FROM_HANDSHAKE}; diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 9ef8c6196f..6da029c88f 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1,6 +1,6 @@ //! Internet Message Format reception pipeline. -use std::collections::HashSet; +use std::collections::{HashSet, HashMap}; use std::iter; use std::sync::LazyLock; @@ -24,8 +24,7 @@ use crate::ephemeral::{stock_ephemeral_timer_changed, Timer as EphemeralTimer}; use crate::events::EventType; use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::imap::{markseen_on_imap_table, GENERATED_PREFIX}; -use crate::key::DcKey; -use crate::key::Fingerprint; +use crate::key::{DcKey, Fingerprint, SignedPublicKey}; use crate::log::LogExt; use crate::message::{ self, rfc724_mid_exists, Message, MessageState, MessengerMessage, MsgId, Viewtype, @@ -317,19 +316,6 @@ pub(crate) async fn receive_imf_inner( } }; - let to_ids = add_or_lookup_contacts_by_address_list( - context, - &mime_parser.recipients, - if !mime_parser.incoming { - Origin::OutgoingTo - } else if incoming_origin.is_known() { - Origin::IncomingTo - } else { - Origin::IncomingUnknownTo - }, - ) - .await?; - let chat_id = if let Some(grpid) = mime_parser.get_chat_group_id() { if let Some((chat_id, _protected, _blocked)) = chat::get_chat_id_by_grpid(context, &grpid).await? @@ -342,10 +328,41 @@ pub(crate) async fn receive_imf_inner( None }; - let past_ids: Vec> = if let Some(chat_id) = chat_id { - lookup_pgp_contacts_by_address_list(context, &mime_parser.past_members, chat_id).await? + let to_ids: Vec>; + let past_ids: Vec>; + + if let Some(chat_id) = chat_id { + to_ids = add_or_lookup_pgp_contacts_by_address_list( + context, + &mime_parser.recipients, + &mime_parser.gossiped_keys, + if !mime_parser.incoming { + Origin::OutgoingTo + } else if incoming_origin.is_known() { + Origin::IncomingTo + } else { + Origin::IncomingUnknownTo + }, + ) + .await?; + + past_ids = lookup_pgp_contacts_by_address_list(context, &mime_parser.past_members, chat_id) + .await?; } else { - add_or_lookup_contacts_by_address_list( + to_ids = add_or_lookup_contacts_by_address_list( + context, + &mime_parser.recipients, + if !mime_parser.incoming { + Origin::OutgoingTo + } else if incoming_origin.is_known() { + Origin::IncomingTo + } else { + Origin::IncomingUnknownTo + }, + ) + .await?; + + past_ids = add_or_lookup_contacts_by_address_list( context, &mime_parser.past_members, if !mime_parser.incoming { @@ -356,10 +373,7 @@ pub(crate) async fn receive_imf_inner( Origin::IncomingUnknownTo }, ) - .await? - .into_iter() - .map(|contact_id| Some(contact_id)) - .collect() + .await?; }; update_verified_keys(context, &mut mime_parser, from_id).await?; @@ -376,7 +390,7 @@ pub(crate) async fn receive_imf_inner( let contact = Contact::get_by_id(context, from_id).await?; mime_parser.peerstate = Peerstate::from_addr(context, contact.get_addr()).await?; } else { - let to_id = to_ids.first().copied().unwrap_or(ContactId::SELF); + let to_id = to_ids.first().copied().flatten().unwrap_or(ContactId::SELF); // handshake may mark contacts as verified and must be processed before chats are created res = observe_securejoin_on_other_device(context, &mime_parser, to_id) .await @@ -403,6 +417,9 @@ pub(crate) async fn receive_imf_inner( received_msg = None; } + // FIXME: do not flatten to_ids + let to_ids: Vec = to_ids.into_iter().filter_map(|x| x).collect(); + let verified_encryption = has_verified_encryption(&mime_parser, from_id)?; if verified_encryption == VerifiedEncryption::Verified { @@ -3170,11 +3187,12 @@ async fn add_or_lookup_contacts_by_address_list( context: &Context, address_list: &[SingleInfo], origin: Origin, -) -> Result> { +) -> Result>> { let mut contact_ids = Vec::new(); for info in address_list { let addr = &info.addr; if !may_be_valid_addr(addr) { + contact_ids.push(None); continue; } let display_name = info.display_name.as_deref(); @@ -3182,9 +3200,44 @@ async fn add_or_lookup_contacts_by_address_list( let (contact_id, _) = Contact::add_or_lookup(context, display_name.unwrap_or_default(), &addr, origin) .await?; - contact_ids.push(contact_id); + contact_ids.push(Some(contact_id)); + } else { + warn!(context, "Contact with address {:?} cannot exist.", addr); + contact_ids.push(None); + } + } + + Ok(contact_ids) +} + +/// Looks up contact IDs from the database given the list of recipients. +async fn add_or_lookup_pgp_contacts_by_address_list( + context: &Context, + address_list: &[SingleInfo], + gossiped_keys: &HashMap, + origin: Origin, +) -> Result>> { + let mut contact_ids = Vec::new(); + for info in address_list { + let addr = &info.addr; + if !may_be_valid_addr(addr) { + contact_ids.push(None); + continue; + } + let Some(key) = gossiped_keys.get(addr) else { + contact_ids.push(None); + continue; + }; + let fingerprint = key.dc_fingerprint().hex(); + let display_name = info.display_name.as_deref(); + if let Ok(addr) = ContactAddress::new(addr) { + let (contact_id, _) = + Contact::add_or_lookup_ex(context, display_name.unwrap_or_default(), &addr, &fingerprint, origin) + .await?; + contact_ids.push(Some(contact_id)); } else { warn!(context, "Contact with address {:?} cannot exist.", addr); + contact_ids.push(None); } } From e735c2ea451659972e63e141988721f5b9c5e680 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 10 Apr 2025 20:23:32 +0000 Subject: [PATCH 010/381] refactor: use add_or_lookup_contact_id --- src/test_utils.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test_utils.rs b/src/test_utils.rs index 85cf61c7aa..1bb0c13183 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -1029,8 +1029,8 @@ impl TestContext { let chat_id = create_group_chat(self, protect, chat_name).await.unwrap(); let mut to_add = vec![]; for member in members { - let contact = self.add_or_lookup_contact(member).await; - to_add.push(contact.id); + let contact_id = self.add_or_lookup_contact_id(member).await; + to_add.push(contact_id); } add_to_chat_contacts_table(self, time(), chat_id, &to_add) .await From b561a772df58c7257aba9149d7265e70c80d35b2 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 10 Apr 2025 20:23:32 +0000 Subject: [PATCH 011/381] format --- src/receive_imf.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 6da029c88f..ba294c0dca 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1,6 +1,6 @@ //! Internet Message Format reception pipeline. -use std::collections::{HashSet, HashMap}; +use std::collections::{HashMap, HashSet}; use std::iter; use std::sync::LazyLock; @@ -3231,9 +3231,14 @@ async fn add_or_lookup_pgp_contacts_by_address_list( let fingerprint = key.dc_fingerprint().hex(); let display_name = info.display_name.as_deref(); if let Ok(addr) = ContactAddress::new(addr) { - let (contact_id, _) = - Contact::add_or_lookup_ex(context, display_name.unwrap_or_default(), &addr, &fingerprint, origin) - .await?; + let (contact_id, _) = Contact::add_or_lookup_ex( + context, + display_name.unwrap_or_default(), + &addr, + &fingerprint, + origin, + ) + .await?; contact_ids.push(Some(contact_id)); } else { warn!(context, "Contact with address {:?} cannot exist.", addr); From 9b5428d7c18fb8a14a44f89fecaea632467deb65 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 10 Apr 2025 20:23:32 +0000 Subject: [PATCH 012/381] Use load_from_db() --- src/chat/chat_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 98089117ec..2deb10e7bf 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -3434,7 +3434,7 @@ async fn test_info_contact_id() -> Result<()> { expected_bob_id: ContactId, ) -> Result<()> { let sent_msg = alice.pop_sent_msg().await; - let msg = Message::load_from_db(alice, sent_msg.sender_msg_id).await?; + let msg = sent_msg.load_from_db().await; assert_eq!(msg.get_info_type(), expected_type); assert_eq!( msg.get_info_contact_id(alice).await?, From beea5affc3ffcc6e5cb30dbad45664b94ee9db21 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 10 Apr 2025 21:57:20 +0000 Subject: [PATCH 013/381] make chat::chat_tests::test_info_contact_id pass --- src/receive_imf.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index ba294c0dca..e364399b11 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -331,7 +331,7 @@ pub(crate) async fn receive_imf_inner( let to_ids: Vec>; let past_ids: Vec>; - if let Some(chat_id) = chat_id { + if mime_parser.get_chat_group_id().is_some() { to_ids = add_or_lookup_pgp_contacts_by_address_list( context, &mime_parser.recipients, @@ -346,8 +346,14 @@ pub(crate) async fn receive_imf_inner( ) .await?; - past_ids = lookup_pgp_contacts_by_address_list(context, &mime_parser.past_members, chat_id) - .await?; + if let Some(chat_id) = chat_id { + past_ids = + lookup_pgp_contacts_by_address_list(context, &mime_parser.past_members, chat_id) + .await?; + } else { + // TODO: lookup by fingerprints if they are available. + past_ids = vec![None; mime_parser.past_members.len()]; + } } else { to_ids = add_or_lookup_contacts_by_address_list( context, From 668e4ebfe8c45bff391c9aa381be9b76f2a6ea96 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 10 Apr 2025 23:27:52 +0000 Subject: [PATCH 014/381] chat::chat_tests::test_info_contact_id passes --- src/receive_imf.rs | 73 +++++++++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 33 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index e364399b11..92e9d46087 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -423,9 +423,6 @@ pub(crate) async fn receive_imf_inner( received_msg = None; } - // FIXME: do not flatten to_ids - let to_ids: Vec = to_ids.into_iter().filter_map(|x| x).collect(); - let verified_encryption = has_verified_encryption(&mime_parser, from_id)?; if verified_encryption == VerifiedEncryption::Verified { @@ -737,7 +734,7 @@ async fn add_parts( context: &Context, mime_parser: &mut MimeMessage, imf_raw: &[u8], - to_ids: &[ContactId], + to_ids: &[Option], past_ids: &[Option], rfc724_mid: &str, from_id: ContactId, @@ -903,7 +900,7 @@ async fn add_parts( context, mime_parser, &parent, - to_ids, + &to_ids, from_id, allow_creation || test_normal_chat.is_some(), create_blocked, @@ -1081,7 +1078,7 @@ async fn add_parts( // the mail is on the IMAP server, probably it is also delivered. // We cannot recreate other states (read, error). state = MessageState::OutDelivered; - to_id = to_ids.first().copied().unwrap_or(ContactId::SELF); + to_id = to_ids.first().copied().flatten().unwrap_or(ContactId::SELF); // Older Delta Chat versions with core <=1.152.2 only accepted // self-sent messages in Saved Messages with own address in the `To` field. @@ -1994,15 +1991,18 @@ async fn lookup_chat_or_create_adhoc_group( context: &Context, mime_parser: &MimeMessage, parent: &Option, - to_ids: &[ContactId], + to_ids: &[Option], from_id: ContactId, allow_creation: bool, create_blocked: Blocked, is_partial_download: bool, ) -> Result> { + // FIXME + let to_ids: Vec = to_ids.iter().copied().filter_map(|x| x).collect(); + if let Some((new_chat_id, new_chat_id_blocked)) = // Try to assign to a chat based on In-Reply-To/References. - lookup_chat_by_reply(context, mime_parser, parent, to_ids, from_id).await? + lookup_chat_by_reply(context, mime_parser, parent, &to_ids, from_id).await? { return Ok(Some((new_chat_id, new_chat_id_blocked))); } @@ -2029,7 +2029,7 @@ async fn lookup_chat_or_create_adhoc_group( .map(|s| remove_subject_prefix(&s)) .unwrap_or_else(|| "👥📧".to_string()); let mut contact_ids = Vec::with_capacity(to_ids.len() + 1); - contact_ids.extend(to_ids); + contact_ids.extend(&to_ids); if !contact_ids.contains(&from_id) { contact_ids.push(from_id); } @@ -2085,7 +2085,7 @@ async fn lookup_chat_or_create_adhoc_group( mime_parser, create_blocked, from_id, - to_ids, + &to_ids, &grpname, ) .await @@ -2141,11 +2141,12 @@ async fn create_group( is_partial_download: bool, create_blocked: Blocked, from_id: ContactId, - to_ids: &[ContactId], + to_ids: &[Option], past_ids: &[Option], verified_encryption: &VerifiedEncryption, grpid: &str, ) -> Result> { + let to_ids_flat: Vec = to_ids.iter().copied().filter_map(|x| x).collect(); let mut chat_id = None; let mut chat_id_blocked = Default::default(); @@ -2153,7 +2154,7 @@ async fn create_group( // they belong to the group because of the Chat-Group-Id or Message-Id header if let Some(chat_id) = chat_id { if !mime_parser.has_chat_version() - && is_probably_private_reply(context, to_ids, from_id, mime_parser, chat_id).await? + && is_probably_private_reply(context, &to_ids_flat, from_id, mime_parser, chat_id).await? { return Ok(None); } @@ -2218,8 +2219,8 @@ async fn create_group( // Create initial member list. if let Some(mut chat_group_member_timestamps) = mime_parser.chat_group_member_timestamps() { let mut new_to_ids = to_ids.to_vec(); - if !new_to_ids.contains(&from_id) { - new_to_ids.insert(0, from_id); + if !new_to_ids.contains(&Some(from_id)) { + new_to_ids.insert(0, Some(from_id)); chat_group_member_timestamps.insert(0, mime_parser.timestamp_sent); } @@ -2237,7 +2238,7 @@ async fn create_group( if !from_id.is_special() { members.push(from_id); } - members.extend(to_ids); + members.extend(to_ids_flat); // Add all members with 0 timestamp // because we don't know the real timestamp of their addition. @@ -2276,7 +2277,7 @@ async fn update_chats_contacts_timestamps( context: &Context, chat_id: ChatId, ignored_id: Option, - to_ids: &[ContactId], + to_ids: &[Option], past_ids: &[Option], chat_group_member_timestamps: &[i64], ) -> Result { @@ -2310,11 +2311,13 @@ async fn update_chats_contacts_timestamps( to_ids.iter(), chat_group_member_timestamps.iter().take(to_ids.len()), ) { - if Some(*contact_id) != ignored_id { - // It could be that member was already added, - // but updated addition timestamp - // is also a modification worth notifying about. - modified |= add_statement.execute((chat_id, contact_id, ts))? > 0; + if let Some(contact_id) = contact_id { + if Some(*contact_id) != ignored_id { + // It could be that member was already added, + // but updated addition timestamp + // is also a modification worth notifying about. + modified |= add_statement.execute((chat_id, contact_id, ts))? > 0; + } } } @@ -2331,10 +2334,12 @@ async fn update_chats_contacts_timestamps( past_ids.iter(), chat_group_member_timestamps.iter().skip(to_ids.len()), ) { - // It could be that member was already removed, - // but updated removal timestamp - // is also a modification worth notifying about. - modified |= remove_statement.execute((chat_id, contact_id, ts))? > 0; + if let Some(contact_id) = contact_id { + // It could be that member was already removed, + // but updated removal timestamp + // is also a modification worth notifying about. + modified |= remove_statement.execute((chat_id, contact_id, ts))? > 0; + } } Ok(()) @@ -2371,10 +2376,12 @@ async fn apply_group_changes( mime_parser: &mut MimeMessage, chat_id: ChatId, from_id: ContactId, - to_ids: &[ContactId], + to_ids: &[Option], past_ids: &[Option], verified_encryption: &VerifiedEncryption, ) -> Result { + // FIXME + let to_ids_flat: Vec = to_ids.iter().copied().filter_map(|x| x).collect(); if chat_id.is_special() { // Do not apply group changes to the trash chat. return Ok(GroupChangesInfo::default()); @@ -2513,7 +2520,7 @@ async fn apply_group_changes( if is_from_in_chat { if chat.member_list_is_stale(context).await? { info!(context, "Member list is stale."); - let mut new_members: HashSet = HashSet::from_iter(to_ids.iter().copied()); + let mut new_members: HashSet = HashSet::from_iter(to_ids_flat.iter().copied()); new_members.insert(ContactId::SELF); if !from_id.is_special() { new_members.insert(from_id); @@ -2549,15 +2556,15 @@ async fn apply_group_changes( context, chat_id, Some(from_id), - to_ids, + &to_ids, past_ids, chat_group_member_timestamps, ) .await?; } else { - let mut new_members; + let mut new_members: HashSet; if self_added { - new_members = HashSet::from_iter(to_ids.iter().copied()); + new_members = HashSet::from_iter(to_ids_flat.iter().copied()); new_members.insert(ContactId::SELF); if !from_id.is_special() { new_members.insert(from_id); @@ -2570,7 +2577,7 @@ async fn apply_group_changes( if mime_parser.get_header(HeaderDef::ChatVersion).is_none() { // Don't delete any members locally, but instead add absent ones to provide group // membership consistency for all members: - new_members.extend(to_ids.iter()); + new_members.extend(to_ids_flat.iter()); } // Apply explicit addition if any. @@ -3122,14 +3129,14 @@ fn has_verified_encryption( async fn mark_recipients_as_verified( context: &Context, from_id: ContactId, - to_ids: &[ContactId], + to_ids: &[Option], mimeparser: &MimeMessage, ) -> Result<()> { if mimeparser.get_header(HeaderDef::ChatVerified).is_none() { return Ok(()); } let contact = Contact::get_by_id(context, from_id).await?; - for &id in to_ids { + for id in to_ids.iter().filter_map(|&x| x) { if id == ContactId::SELF { continue; } From bcccd9a7d2223edac7796ad5948b9044093d5c48 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 11 Apr 2025 01:11:36 +0000 Subject: [PATCH 015/381] test: add sections to chat::chat_tests::test_past_members --- src/chat/chat_tests.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 2deb10e7bf..0161c35953 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -3597,19 +3597,24 @@ async fn test_past_members() -> Result<()> { let fiona = &tcm.fiona().await; let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await; + tcm.section("Alice creates a chat."); let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?; add_contact_to_chat(alice, alice_chat_id, alice_fiona_contact_id).await?; alice .send_text(alice_chat_id, "Hi! I created a group.") .await; + + tcm.section("Alice removes Fiona from the chat."); remove_contact_from_chat(alice, alice_chat_id, alice_fiona_contact_id).await?; assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 1); + tcm.section("Alice adds Bob to the chat."); let bob = &tcm.bob().await; let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await; add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?; + tcm.section("Bob receives a message."); let add_message = alice.pop_sent_msg().await; let bob_add_message = bob.recv_msg(&add_message).await; let bob_chat_id = bob_add_message.chat_id; From 17f9a1464929f55040e641b462601911cc3129e3 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 11 Apr 2025 04:01:12 +0000 Subject: [PATCH 016/381] simplify peerstate loading --- src/mimefactory.rs | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index eae2593ea1..544081473a 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -374,20 +374,6 @@ impl MimeFactory { Ok(res) } - async fn peerstates_for_recipients( - &self, - context: &Context, - ) -> Result, String)>> { - let self_addr = context.get_primary_self_addr().await?; - - let mut res = Vec::new(); - for addr in self.recipients.iter().filter(|&addr| *addr != self_addr) { - res.push((Peerstate::from_addr(context, addr).await?, addr.clone())); - } - - Ok(res) - } - fn is_e2ee_guaranteed(&self) -> bool { match &self.loaded { Loaded::Message { chat, msg } => { @@ -754,7 +740,6 @@ impl MimeFactory { } let is_chatmail = context.is_chatmail().await?; - let peerstates = self.peerstates_for_recipients(context).await?; let is_encrypted = !self.should_force_plaintext() && (e2ee_guaranteed || is_chatmail @@ -941,6 +926,11 @@ impl MimeFactory { message.header(header, value) }); + let mut peerstates = Vec::new(); + for addr in self.recipients.iter().filter(|&addr| *addr != self.from_addr) { + peerstates.push((Peerstate::from_addr(context, addr).await?, addr.clone())); + } + // Add gossip headers in chats with multiple recipients let multiple_recipients = peerstates.len() > 1 || context.get_config_bool(Config::BccSelf).await?; From 87e536c9a52e7bdab4e141d33cf89a8391ae6081 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 11 Apr 2025 04:01:12 +0000 Subject: [PATCH 017/381] format --- src/receive_imf.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 92e9d46087..d26162475f 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -2154,7 +2154,8 @@ async fn create_group( // they belong to the group because of the Chat-Group-Id or Message-Id header if let Some(chat_id) = chat_id { if !mime_parser.has_chat_version() - && is_probably_private_reply(context, &to_ids_flat, from_id, mime_parser, chat_id).await? + && is_probably_private_reply(context, &to_ids_flat, from_id, mime_parser, chat_id) + .await? { return Ok(None); } @@ -2520,7 +2521,8 @@ async fn apply_group_changes( if is_from_in_chat { if chat.member_list_is_stale(context).await? { info!(context, "Member list is stale."); - let mut new_members: HashSet = HashSet::from_iter(to_ids_flat.iter().copied()); + let mut new_members: HashSet = + HashSet::from_iter(to_ids_flat.iter().copied()); new_members.insert(ContactId::SELF); if !from_id.is_special() { new_members.insert(from_id); From 33be4bad2610339e6a494c3d6051627cb73dbedf Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 11 Apr 2025 04:01:12 +0000 Subject: [PATCH 018/381] simplify peerstate loading --- src/e2ee.rs | 67 ---------------------------------------------- src/mimefactory.rs | 51 ++++++++++++++++++++++++----------- 2 files changed, 36 insertions(+), 82 deletions(-) diff --git a/src/e2ee.rs b/src/e2ee.rs index 011e8b8c06..315832974f 100644 --- a/src/e2ee.rs +++ b/src/e2ee.rs @@ -62,73 +62,6 @@ impl EncryptHelper { Ok(true) } - /// Constructs a vector of public keys for given peerstates. - /// - /// In addition returns the set of recipient addresses - /// for which there is no key available. - /// - /// Returns an error if there are recipients - /// other than self, but no recipient keys are available. - pub(crate) fn encryption_keyring( - &self, - context: &Context, - verified: bool, - peerstates: &[(Option, String)], - ) -> Result<(Vec, BTreeSet)> { - // Encrypt to self unconditionally, - // even for a single-device setup. - let mut keyring = vec![self.public_key.clone()]; - let mut missing_key_addresses = BTreeSet::new(); - - if peerstates.is_empty() { - return Ok((keyring, missing_key_addresses)); - } - - let mut verifier_addresses: Vec<&str> = Vec::new(); - - for (peerstate, addr) in peerstates { - if let Some(peerstate) = peerstate { - if let Some(key) = peerstate.clone().take_key(verified) { - keyring.push(key); - verifier_addresses.push(addr); - } else { - warn!(context, "Encryption key for {addr} is missing."); - missing_key_addresses.insert(addr.clone()); - } - } else { - warn!(context, "Peerstate for {addr} is missing."); - missing_key_addresses.insert(addr.clone()); - } - } - - debug_assert!( - !keyring.is_empty(), - "At least our own key is in the keyring" - ); - if keyring.len() <= 1 { - bail!("No recipient keys are available, cannot encrypt"); - } - - // Encrypt to secondary verified keys - // if we also encrypt to the introducer ("verifier") of the key. - if verified { - for (peerstate, _addr) in peerstates { - if let Some(peerstate) = peerstate { - if let (Some(key), Some(verifier)) = ( - peerstate.secondary_verified_key.as_ref(), - peerstate.secondary_verifier.as_deref(), - ) { - if verifier_addresses.contains(&verifier) { - keyring.push(key.clone()); - } - } - } - } - } - - Ok((keyring, missing_key_addresses)) - } - /// Tries to encrypt the passed in `mail`. pub async fn encrypt( self, diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 544081473a..4bccb76e78 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1,6 +1,6 @@ //! # MIME message production. -use std::collections::HashSet; +use std::collections::{HashSet, BTreeSet}; use std::io::Cursor; use std::path::Path; @@ -23,7 +23,7 @@ use crate::contact::{Contact, ContactId, Origin}; use crate::context::Context; use crate::e2ee::EncryptHelper; use crate::ephemeral::Timer as EphemeralTimer; -use crate::key::DcKey; +use crate::key::{DcKey, SignedPublicKey}; use crate::location; use crate::message::{self, Message, MsgId, Viewtype}; use crate::mimeparser::{is_hidden, SystemMessage}; @@ -926,14 +926,37 @@ impl MimeFactory { message.header(header, value) }); - let mut peerstates = Vec::new(); - for addr in self.recipients.iter().filter(|&addr| *addr != self.from_addr) { - peerstates.push((Peerstate::from_addr(context, addr).await?, addr.clone())); + let mut additional_encryption_keyring = Vec::new(); + let mut missing_key_addresses = BTreeSet::new(); + + for addr in self + .recipients + .iter() + .filter(|&addr| *addr != self.from_addr) + { + let peerstate_opt = Peerstate::from_addr(context, addr).await?; + let Some(peerstate) = peerstate_opt else { + warn!(context, "Peerstate for {addr} is missing."); + missing_key_addresses.insert(addr.clone()); + continue; + }; + + let key_opt: Option = peerstate.take_key(verified); + if let Some(key) = key_opt { + additional_encryption_keyring.push((addr.clone(), key)); + } else { + warn!(context, "Encryption key for {addr} is missing."); + missing_key_addresses.insert(addr.clone()); + } + } + + if additional_encryption_keyring.is_empty() { + bail!("No recipient keys are available, cannot encrypt"); } // Add gossip headers in chats with multiple recipients - let multiple_recipients = - peerstates.len() > 1 || context.get_config_bool(Config::BccSelf).await?; + let multiple_recipients = additional_encryption_keyring.len() > 1 + || context.get_config_bool(Config::BccSelf).await?; let gossip_period = context.get_config_i64(Config::GossipPeriod).await?; let now = time(); @@ -941,11 +964,7 @@ impl MimeFactory { match &self.loaded { Loaded::Message { chat, msg } => { if chat.typ != Chattype::Broadcast { - for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) { - let Some(key) = peerstate.peek_key(verified) else { - continue; - }; - + for (addr, key) in &additional_encryption_keyring { let fingerprint = key.dc_fingerprint().hex(); let cmd = msg.param.get_cmd(); let should_do_gossip = cmd == SystemMessage::MemberAddedToGroup @@ -977,7 +996,7 @@ impl MimeFactory { } let header = Aheader::new( - peerstate.addr.clone(), + addr.clone(), key.clone(), // Autocrypt 1.1.0 specification says that // `prefer-encrypt` attribute SHOULD NOT be included. @@ -1027,8 +1046,10 @@ impl MimeFactory { Loaded::Mdn { .. } => true, }; - let (encryption_keyring, missing_key_addresses) = - encrypt_helper.encryption_keyring(context, verified, &peerstates)?; + // Encrypt to self unconditionally, + // even for a single-device setup. + let mut encryption_keyring = vec![encrypt_helper.public_key.clone()]; + encryption_keyring.extend(additional_encryption_keyring.iter().map(|(addr, key)| (*key).clone())); // XXX: additional newline is needed // to pass filtermail at From fc177b2c2b8672ae7d3236cc0108110388b6bc3a Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 11 Apr 2025 15:40:17 +0000 Subject: [PATCH 019/381] MimeFactory.encryption_certificates --- src/contact.rs | 14 ++++ src/mimefactory.rs | 187 +++++++++++++++++++++++++-------------------- 2 files changed, 120 insertions(+), 81 deletions(-) diff --git a/src/contact.rs b/src/contact.rs index 2a8b4dc3fa..111b54c1c4 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -1435,6 +1435,20 @@ impl Contact { self.fingerprint.as_deref() } + /// Returns OpenPGP certificate of a contact. + pub async fn openpgp_certificate(&self, context: &Context) -> Result> { + if self.fingerprint.is_some() { + // TODO: load by fingerprint from a key table. + let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? else { + return Ok(None); + }; + let key_opt: Option = peerstate.take_key(false); + Ok(key_opt) + } else { + Ok(None) + } + } + /// Get name authorized by the contact. pub fn get_authname(&self) -> &str { &self.authname diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 4bccb76e78..6a1f559d52 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1,6 +1,6 @@ //! # MIME message production. -use std::collections::{HashSet, BTreeSet}; +use std::collections::{BTreeSet, HashSet}; use std::io::Cursor; use std::path::Path; @@ -86,6 +86,12 @@ pub struct MimeFactory { /// but `MimeFactory` is not responsible for this. recipients: Vec, + /// Vector of recipient OpenPGP certificates + /// to use for encryption. + /// + /// `None` if the message is not encrypted. + encryption_certificates: Option>, + /// Vector of pairs of recipient name and address that goes into the `To` field. /// /// The list of actual message recipient addresses may be different, @@ -318,12 +324,87 @@ impl MimeFactory { member_timestamps.is_empty() || to.len() + past_members.len() == member_timestamps.len() ); + + let should_force_plaintext = msg + .param + .get_bool(Param::ForcePlaintext) + .unwrap_or_default(); + let encryption_certificates = if should_force_plaintext { + None + } else { + let is_chatmail = context.is_chatmail().await?; + let e2ee_guaranteed = is_chatmail + || (!msg + .param + .get_bool(Param::ForcePlaintext) + .unwrap_or_default() + && (chat.is_protected() + || msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default())); + let is_encrypted = e2ee_guaranteed + || match chat.typ { + Chattype::Single => { + let chat_contact_ids = get_chat_contacts(context, chat.id).await?; + if let Some(contact_id) = chat_contact_ids.first() { + let contact = Contact::get_by_id(context, *contact_id).await?; + contact.is_pgp_contact() + } else { + true + } + } + Chattype::Group => { + // Do not encrypt ad-hoc groups. + !chat.grpid.is_empty() + } + Chattype::Mailinglist => false, + Chattype::Broadcast => true, + }; + + if is_encrypted { + let mut additional_encryption_keyring = Vec::new(); + let mut missing_key_addresses = BTreeSet::new(); + + for addr in recipients.iter().filter(|&addr| *addr != from_addr) { + let peerstate_opt = Peerstate::from_addr(context, addr).await?; + let Some(peerstate) = peerstate_opt else { + warn!(context, "Peerstate for {addr} is missing."); + missing_key_addresses.insert(addr.clone()); + continue; + }; + + // TODO: PGP-contact has only one key, verified or not + let verified = false; + + let key_opt: Option = peerstate.take_key(verified); + if let Some(key) = key_opt { + additional_encryption_keyring.push((addr.clone(), key)); + } else { + warn!(context, "Encryption key for {addr} is missing."); + missing_key_addresses.insert(addr.clone()); + } + } + + if additional_encryption_keyring.is_empty() { + bail!("No recipient keys are available, cannot encrypt"); + } + + // Remove recipients for which the key is missing. + if !missing_key_addresses.is_empty() { + recipients.retain(|addr| !missing_key_addresses.contains(addr)); + } + + Some(additional_encryption_keyring) + } else { + None + } + }; + let factory = MimeFactory { from_addr, from_displayname, sender_displayname, selfstatus, recipients, + encryption_certificates, to, past_members, member_timestamps, @@ -349,12 +430,24 @@ impl MimeFactory { let from_addr = context.get_primary_self_addr().await?; let timestamp = create_smeared_timestamp(context); + let addr = contact.get_addr().to_string(); + let encryption_certificates = if contact.is_pgp_contact() { + if let Some(openpgp_certificate) = contact.openpgp_certificate(context).await? { + Some(vec![(addr.clone(), openpgp_certificate)]) + } else { + Some(Vec::new()) + } + } else { + None + }; + let res = MimeFactory { from_addr, from_displayname: "".to_string(), sender_displayname: None, selfstatus: "".to_string(), - recipients: vec![contact.get_addr().to_string()], + recipients: vec![addr], + encryption_certificates, to: vec![("".to_string(), contact.get_addr().to_string())], past_members: vec![], member_timestamps: vec![], @@ -374,19 +467,6 @@ impl MimeFactory { Ok(res) } - fn is_e2ee_guaranteed(&self) -> bool { - match &self.loaded { - Loaded::Message { chat, msg } => { - !msg.param - .get_bool(Param::ForcePlaintext) - .unwrap_or_default() - && (chat.is_protected() - || msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default()) - } - Loaded::Mdn { .. } => false, - } - } - fn verified(&self) -> bool { match &self.loaded { Loaded::Message { chat, msg } => { @@ -714,7 +794,6 @@ impl MimeFactory { let verified = self.verified(); let grpimage = self.grpimage(); let skip_autocrypt = self.should_skip_autocrypt(); - let e2ee_guaranteed = self.is_e2ee_guaranteed(); let encrypt_helper = EncryptHelper::new(context).await?; if !skip_autocrypt { @@ -726,6 +805,8 @@ impl MimeFactory { )); } + let is_encrypted = self.encryption_certificates.is_some(); + // Add ephemeral timer for non-MDN messages. // For MDNs it does not matter because they are not visible // and ignored by the receiver. @@ -739,32 +820,6 @@ impl MimeFactory { } } - let is_chatmail = context.is_chatmail().await?; - let is_encrypted = !self.should_force_plaintext() - && (e2ee_guaranteed - || is_chatmail - || match &self.loaded { - Loaded::Message { chat, .. } => { - match chat.typ { - Chattype::Single => { - let chat_contact_ids = get_chat_contacts(context, chat.id).await?; - if let Some(contact_id) = chat_contact_ids.first() { - let contact = Contact::get_by_id(context, *contact_id).await?; - contact.is_pgp_contact() - } else { - true - } - } - Chattype::Group => { - // Do not encrypt ad-hoc groups. - !chat.grpid.is_empty() - } - Chattype::Mailinglist => false, - Chattype::Broadcast => true, - } - } - Loaded::Mdn { .. } => true, - }); let is_securejoin_message = if let Loaded::Message { msg, .. } = &self.loaded { msg.param.get_cmd() == SystemMessage::SecurejoinMessage } else { @@ -911,7 +966,7 @@ impl MimeFactory { } } - let outer_message = if is_encrypted { + let outer_message = if let Some(encryption_certificates) = self.encryption_certificates { // Store protected headers in the inner message. let message = protected_headers .into_iter() @@ -926,36 +981,8 @@ impl MimeFactory { message.header(header, value) }); - let mut additional_encryption_keyring = Vec::new(); - let mut missing_key_addresses = BTreeSet::new(); - - for addr in self - .recipients - .iter() - .filter(|&addr| *addr != self.from_addr) - { - let peerstate_opt = Peerstate::from_addr(context, addr).await?; - let Some(peerstate) = peerstate_opt else { - warn!(context, "Peerstate for {addr} is missing."); - missing_key_addresses.insert(addr.clone()); - continue; - }; - - let key_opt: Option = peerstate.take_key(verified); - if let Some(key) = key_opt { - additional_encryption_keyring.push((addr.clone(), key)); - } else { - warn!(context, "Encryption key for {addr} is missing."); - missing_key_addresses.insert(addr.clone()); - } - } - - if additional_encryption_keyring.is_empty() { - bail!("No recipient keys are available, cannot encrypt"); - } - // Add gossip headers in chats with multiple recipients - let multiple_recipients = additional_encryption_keyring.len() > 1 + let multiple_recipients = encryption_certificates.len() > 1 || context.get_config_bool(Config::BccSelf).await?; let gossip_period = context.get_config_i64(Config::GossipPeriod).await?; @@ -964,7 +991,7 @@ impl MimeFactory { match &self.loaded { Loaded::Message { chat, msg } => { if chat.typ != Chattype::Broadcast { - for (addr, key) in &additional_encryption_keyring { + for (addr, key) in &encryption_certificates { let fingerprint = key.dc_fingerprint().hex(); let cmd = msg.param.get_cmd(); let should_do_gossip = cmd == SystemMessage::MemberAddedToGroup @@ -1049,7 +1076,11 @@ impl MimeFactory { // Encrypt to self unconditionally, // even for a single-device setup. let mut encryption_keyring = vec![encrypt_helper.public_key.clone()]; - encryption_keyring.extend(additional_encryption_keyring.iter().map(|(addr, key)| (*key).clone())); + encryption_keyring.extend( + encryption_certificates + .iter() + .map(|(addr, key)| (*key).clone()), + ); // XXX: additional newline is needed // to pass filtermail at @@ -1059,12 +1090,6 @@ impl MimeFactory { .await? + "\n"; - // Remove recipients for which the key is missing. - if !missing_key_addresses.is_empty() { - self.recipients - .retain(|addr| !missing_key_addresses.contains(addr)); - } - // Set the appropriate Content-Type for the outer message MimePart::new( "multipart/encrypted; protocol=\"application/pgp-encrypted\"", @@ -1594,7 +1619,7 @@ impl MimeFactory { // we do not piggyback sync-files to other self-sent-messages // to not risk files becoming too larger and being skipped by download-on-demand. - if command == SystemMessage::MultiDeviceSync && self.is_e2ee_guaranteed() { + if command == SystemMessage::MultiDeviceSync { let json = msg.param.get(Param::Arg).unwrap_or_default(); let ids = msg.param.get(Param::Arg2).unwrap_or_default(); parts.push(context.build_sync_part(json.to_string())); From abd8def141d7a7e0d499bba814b47c90754acabb Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 11 Apr 2025 16:40:34 +0000 Subject: [PATCH 020/381] rename variable --- src/mimefactory.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 6a1f559d52..4b9e56e508 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -360,7 +360,7 @@ impl MimeFactory { }; if is_encrypted { - let mut additional_encryption_keyring = Vec::new(); + let mut certificates = Vec::new(); let mut missing_key_addresses = BTreeSet::new(); for addr in recipients.iter().filter(|&addr| *addr != from_addr) { @@ -376,14 +376,14 @@ impl MimeFactory { let key_opt: Option = peerstate.take_key(verified); if let Some(key) = key_opt { - additional_encryption_keyring.push((addr.clone(), key)); + certificates.push((addr.clone(), key)); } else { warn!(context, "Encryption key for {addr} is missing."); missing_key_addresses.insert(addr.clone()); } } - if additional_encryption_keyring.is_empty() { + if certificates.is_empty() { bail!("No recipient keys are available, cannot encrypt"); } @@ -392,7 +392,7 @@ impl MimeFactory { recipients.retain(|addr| !missing_key_addresses.contains(addr)); } - Some(additional_encryption_keyring) + Some(certificates) } else { None } From 26d8af1ad12cc61f505fdf825653c294ee2c4a44 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 11 Apr 2025 17:28:52 +0000 Subject: [PATCH 021/381] move encryption_certificates generation --- src/mimefactory.rs | 158 +++++++++++++++++++++------------------------ 1 file changed, 75 insertions(+), 83 deletions(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 4b9e56e508..3088e91215 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -186,6 +186,11 @@ impl MimeFactory { (name, None) }; + let should_force_plaintext = msg + .param + .get_bool(Param::ForcePlaintext) + .unwrap_or_default(); + let mut recipients = Vec::new(); let mut to = Vec::new(); let mut past_members = Vec::new(); @@ -193,8 +198,13 @@ impl MimeFactory { let mut recipient_ids = HashSet::new(); let mut req_mdn = false; + let encryption_certificates; + if chat.is_self_talk() { to.push((from_displayname.to_string(), from_addr.to_string())); + + // Encrypt, but only to self. + encryption_certificates = Some(Vec::new()); } else if chat.is_mailing_list() { let list_post = chat .param @@ -202,6 +212,9 @@ impl MimeFactory { .context("Can't write to mailinglist without ListPost param")?; to.push(("".to_string(), list_post.to_string())); recipients.push(list_post.to_string()); + + // Do not encrypt messages to mailing lists. + encryption_certificates = None; } else { let email_to_remove = if msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup { msg.param.get(Param::Arg) @@ -291,6 +304,68 @@ impl MimeFactory { { req_mdn = true; } + + encryption_certificates = if should_force_plaintext { + None + } else { + let is_encrypted = chat.is_protected() + || msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default() + || match chat.typ { + Chattype::Single => { + let chat_contact_ids = get_chat_contacts(context, chat.id).await?; + if let Some(contact_id) = chat_contact_ids.first() { + let contact = Contact::get_by_id(context, *contact_id).await?; + contact.is_pgp_contact() + } else { + true + } + } + Chattype::Group => { + // Do not encrypt ad-hoc groups. + !chat.grpid.is_empty() + } + Chattype::Mailinglist => false, + Chattype::Broadcast => true, + }; + + if is_encrypted { + let mut certificates = Vec::new(); + let mut missing_key_addresses = BTreeSet::new(); + + for addr in recipients.iter().filter(|&addr| *addr != from_addr) { + let peerstate_opt = Peerstate::from_addr(context, addr).await?; + let Some(peerstate) = peerstate_opt else { + warn!(context, "Peerstate for {addr} is missing."); + missing_key_addresses.insert(addr.clone()); + continue; + }; + + // TODO: PGP-contact has only one key, verified or not + let verified = false; + + let key_opt: Option = peerstate.take_key(verified); + if let Some(key) = key_opt { + certificates.push((addr.clone(), key)); + } else { + warn!(context, "Encryption key for {addr} is missing."); + missing_key_addresses.insert(addr.clone()); + } + } + + if certificates.is_empty() { + bail!("No recipient keys are available, cannot encrypt"); + } + + // Remove recipients for which the key is missing. + if !missing_key_addresses.is_empty() { + recipients.retain(|addr| !missing_key_addresses.contains(addr)); + } + + Some(certificates) + } else { + None + } + }; } let (in_reply_to, references) = context .sql @@ -325,79 +400,6 @@ impl MimeFactory { || to.len() + past_members.len() == member_timestamps.len() ); - let should_force_plaintext = msg - .param - .get_bool(Param::ForcePlaintext) - .unwrap_or_default(); - let encryption_certificates = if should_force_plaintext { - None - } else { - let is_chatmail = context.is_chatmail().await?; - let e2ee_guaranteed = is_chatmail - || (!msg - .param - .get_bool(Param::ForcePlaintext) - .unwrap_or_default() - && (chat.is_protected() - || msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default())); - let is_encrypted = e2ee_guaranteed - || match chat.typ { - Chattype::Single => { - let chat_contact_ids = get_chat_contacts(context, chat.id).await?; - if let Some(contact_id) = chat_contact_ids.first() { - let contact = Contact::get_by_id(context, *contact_id).await?; - contact.is_pgp_contact() - } else { - true - } - } - Chattype::Group => { - // Do not encrypt ad-hoc groups. - !chat.grpid.is_empty() - } - Chattype::Mailinglist => false, - Chattype::Broadcast => true, - }; - - if is_encrypted { - let mut certificates = Vec::new(); - let mut missing_key_addresses = BTreeSet::new(); - - for addr in recipients.iter().filter(|&addr| *addr != from_addr) { - let peerstate_opt = Peerstate::from_addr(context, addr).await?; - let Some(peerstate) = peerstate_opt else { - warn!(context, "Peerstate for {addr} is missing."); - missing_key_addresses.insert(addr.clone()); - continue; - }; - - // TODO: PGP-contact has only one key, verified or not - let verified = false; - - let key_opt: Option = peerstate.take_key(verified); - if let Some(key) = key_opt { - certificates.push((addr.clone(), key)); - } else { - warn!(context, "Encryption key for {addr} is missing."); - missing_key_addresses.insert(addr.clone()); - } - } - - if certificates.is_empty() { - bail!("No recipient keys are available, cannot encrypt"); - } - - // Remove recipients for which the key is missing. - if !missing_key_addresses.is_empty() { - recipients.retain(|addr| !missing_key_addresses.contains(addr)); - } - - Some(certificates) - } else { - None - } - }; - let factory = MimeFactory { from_addr, from_displayname, @@ -481,16 +483,6 @@ impl MimeFactory { } } - fn should_force_plaintext(&self) -> bool { - match &self.loaded { - Loaded::Message { msg, .. } => msg - .param - .get_bool(Param::ForcePlaintext) - .unwrap_or_default(), - Loaded::Mdn { .. } => false, - } - } - fn should_skip_autocrypt(&self) -> bool { match &self.loaded { Loaded::Message { msg, .. } => { From 49450c0b74f30d09781529e63ee14bd079f2d41c Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 11 Apr 2025 18:36:21 +0000 Subject: [PATCH 022/381] Remove pgp_contacts_addr from the migration It will be needed only with multi-transport --- src/sql/migrations.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 85d1c389c4..dd5c9a105d 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1243,12 +1243,6 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint); SELECT verified_key_fingerprint, verified_key FROM acpeerstates; INSERT OR IGNORE INTO public_keys (fingerprint, public_key) SELECT secondary_verified_key_fingerprint, secondary_verified_key FROM acpeerstates; - - CREATE TABLE pgp_contacts_addr ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - contact_id INTEGER NOT NULL UNIQUE, -- ID of a PGP-contact. - addr TEXT NOT NULL -- Email address. - ) STRICT; ", migration_version, ) From 50d38ac52537b803a7d122ad45bfc1b1b27011bf Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 11 Apr 2025 18:36:21 +0000 Subject: [PATCH 023/381] Take OpenPGP certificates from a public key table --- src/contact.rs | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/contact.rs b/src/contact.rs index 111b54c1c4..d5112f818c 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -1436,14 +1436,32 @@ impl Contact { } /// Returns OpenPGP certificate of a contact. + /// + /// Returns `None` if the contact is not a PGP-contact + /// or if the key is not available. + /// It is possible for a PGP-contact to not have a certificate, + /// e.g. if only the fingerprint is known from a QR-code. pub async fn openpgp_certificate(&self, context: &Context) -> Result> { - if self.fingerprint.is_some() { - // TODO: load by fingerprint from a key table. - let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? else { - return Ok(None); - }; - let key_opt: Option = peerstate.take_key(false); - Ok(key_opt) + if let Some(fingerprint) = &self.fingerprint { + if let Some(certificate_bytes) = context + .sql + .query_row_optional( + "SELECT public_key + FROM public_keys + WHERE fingerprint=?", + (fingerprint,), + |row| { + let bytes: Vec = row.get(0)?; + Ok(bytes) + }, + ) + .await? + { + let certificate = SignedPublicKey::from_slice(&certificate_bytes)?; + Ok(Some(certificate)) + } else { + Ok(None) + } } else { Ok(None) } From a39633f193f7655281de13d0502c9664cabb96bc Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 11 Apr 2025 19:00:09 +0000 Subject: [PATCH 024/381] Save public key from a vCard --- src/contact.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/contact.rs b/src/contact.rs index d5112f818c..c955253e7c 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -356,6 +356,17 @@ async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Resu context.emit_event(EventType::ContactsChanged(Some(id))); } if let Some(public_key) = key { + let 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 timestamp = contact .timestamp .as_ref() From 3d729a9a40bab2e4cebc493ac063cac5a5f0091a Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 11 Apr 2025 19:08:29 +0000 Subject: [PATCH 025/381] Take keys from public_keys table --- src/mimefactory.rs | 123 +++++++++++++++++++++------------------------ 1 file changed, 56 insertions(+), 67 deletions(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 3088e91215..6ddd00689c 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -186,11 +186,6 @@ impl MimeFactory { (name, None) }; - let should_force_plaintext = msg - .param - .get_bool(Param::ForcePlaintext) - .unwrap_or_default(); - let mut recipients = Vec::new(); let mut to = Vec::new(); let mut past_members = Vec::new(); @@ -222,12 +217,43 @@ impl MimeFactory { None }; + let is_encrypted = if msg + .param + .get_bool(Param::ForcePlaintext) + .unwrap_or_default() + { + false + } else { + chat.is_protected() + || msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default() + || match chat.typ { + Chattype::Single => { + let chat_contact_ids = get_chat_contacts(context, chat.id).await?; + if let Some(contact_id) = chat_contact_ids.first() { + let contact = Contact::get_by_id(context, *contact_id).await?; + contact.is_pgp_contact() + } else { + true + } + } + Chattype::Group => { + // Do not encrypt ad-hoc groups. + !chat.grpid.is_empty() + } + Chattype::Mailinglist => false, + Chattype::Broadcast => true, + } + }; + + let mut certificates = Vec::new(); + let mut missing_key_addresses = BTreeSet::new(); context .sql .query_map( - "SELECT c.authname, c.addr, c.id, cc.add_timestamp, cc.remove_timestamp + "SELECT c.authname, c.addr, c.id, cc.add_timestamp, cc.remove_timestamp, k.public_key FROM chats_contacts cc LEFT JOIN contacts c ON cc.contact_id=c.id + LEFT JOIN public_keys k ON k.fingerprint=c.fingerprint WHERE cc.chat_id=? AND (cc.contact_id>9 OR (cc.contact_id=1 AND ?))", (msg.chat_id, chat.typ == Chattype::Group), |row| { @@ -236,13 +262,21 @@ impl MimeFactory { let id: ContactId = row.get(2)?; let add_timestamp: i64 = row.get(3)?; let remove_timestamp: i64 = row.get(4)?; - Ok((authname, addr, id, add_timestamp, remove_timestamp)) + let certificate_bytes_opt: Option> = row.get(5)?; + Ok((authname, addr, id, add_timestamp, remove_timestamp, certificate_bytes_opt)) }, |rows| { let mut past_member_timestamps = Vec::new(); for row in rows { - let (authname, addr, id, add_timestamp, remove_timestamp) = row?; + let (authname, addr, id, add_timestamp, remove_timestamp, certificate_bytes) = row?; + + let certificate_opt = if let Some(certificate_bytes) = certificate_bytes { + Some(SignedPublicKey::from_slice(&certificate_bytes)?) + } else { + None + }; + let addr = if id == ContactId::SELF { from_addr.to_string() } else { @@ -256,7 +290,7 @@ impl MimeFactory { if !recipients_contain_addr(&to, &addr) { recipients.push(addr.clone()); if !undisclosed_recipients { - to.push((name, addr)); + to.push((name, addr.clone())); member_timestamps.push(add_timestamp); } } @@ -274,11 +308,17 @@ impl MimeFactory { } } if !undisclosed_recipients { - past_members.push((name, addr)); + past_members.push((name, addr.clone())); past_member_timestamps.push(remove_timestamp); } } } + + if let Some(certificate) = certificate_opt { + certificates.push((addr.clone(), certificate)) + } else { + missing_key_addresses.insert(addr.clone()); + } } debug_assert!(member_timestamps.len() >= to.len()); @@ -305,66 +345,15 @@ impl MimeFactory { req_mdn = true; } - encryption_certificates = if should_force_plaintext { + encryption_certificates = if !is_encrypted { None } else { - let is_encrypted = chat.is_protected() - || msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default() - || match chat.typ { - Chattype::Single => { - let chat_contact_ids = get_chat_contacts(context, chat.id).await?; - if let Some(contact_id) = chat_contact_ids.first() { - let contact = Contact::get_by_id(context, *contact_id).await?; - contact.is_pgp_contact() - } else { - true - } - } - Chattype::Group => { - // Do not encrypt ad-hoc groups. - !chat.grpid.is_empty() - } - Chattype::Mailinglist => false, - Chattype::Broadcast => true, - }; - - if is_encrypted { - let mut certificates = Vec::new(); - let mut missing_key_addresses = BTreeSet::new(); - - for addr in recipients.iter().filter(|&addr| *addr != from_addr) { - let peerstate_opt = Peerstate::from_addr(context, addr).await?; - let Some(peerstate) = peerstate_opt else { - warn!(context, "Peerstate for {addr} is missing."); - missing_key_addresses.insert(addr.clone()); - continue; - }; - - // TODO: PGP-contact has only one key, verified or not - let verified = false; - - let key_opt: Option = peerstate.take_key(verified); - if let Some(key) = key_opt { - certificates.push((addr.clone(), key)); - } else { - warn!(context, "Encryption key for {addr} is missing."); - missing_key_addresses.insert(addr.clone()); - } - } - - if certificates.is_empty() { - bail!("No recipient keys are available, cannot encrypt"); - } - - // Remove recipients for which the key is missing. - if !missing_key_addresses.is_empty() { - recipients.retain(|addr| !missing_key_addresses.contains(addr)); - } - - Some(certificates) - } else { - None + // Remove recipients for which the key is missing. + if !missing_key_addresses.is_empty() { + recipients.retain(|addr| !missing_key_addresses.contains(addr)); } + + Some(certificates) }; } let (in_reply_to, references) = context From e6273bde1069af8d20e55117084298d546ffd6ef Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 11 Apr 2025 20:22:16 +0000 Subject: [PATCH 026/381] no peerstate in mimefactory and contact anymore --- src/contact.rs | 206 ++++++++++-------------------------------- src/mimefactory.rs | 1 - src/sql/migrations.rs | 5 + 3 files changed, 51 insertions(+), 161 deletions(-) diff --git a/src/contact.rs b/src/contact.rs index c955253e7c..2ff725e570 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -33,7 +33,6 @@ use crate::log::LogExt; use crate::message::MessageState; use crate::mimeparser::AvatarAction; use crate::param::{Param, Params}; -use crate::peerstate::Peerstate; use crate::sync::{self, Sync::*}; use crate::tools::{duration_to_str, get_abs_path, smeared_time, time, SystemTime}; use crate::{chat, chatlist_events, stock_str}; @@ -265,13 +264,7 @@ pub async fn make_vcard(context: &Context, contacts: &[ContactId]) -> Result Some(load_self_public_key(context).await?), - _ => Peerstate::from_addr(context, &c.addr) - .await? - .and_then(|peerstate| peerstate.take_key(false)), - }; - let key = key.map(|k| k.to_base64()); + let key = c.openpgp_certificate(context).await?.map(|k| k.to_base64()); let profile_image = match c.get_profile_image(context).await? { None => None, Some(path) => tokio::fs::read(path) @@ -338,12 +331,30 @@ async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Resu .log_err(context) .ok() }); - let fingerprint = key.as_ref().map(|k| k.dc_fingerprint().hex()); + + 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?; + } else { + fingerprint = String::new(); + } + let (id, modified) = match Contact::add_or_lookup_ex( context, &contact.authname, &addr, - &fingerprint.unwrap_or_default(), + &fingerprint, origin, ) .await @@ -355,62 +366,6 @@ async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Resu if modified != Modifier::None { context.emit_event(EventType::ContactsChanged(Some(id))); } - if let Some(public_key) = key { - let 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 timestamp = contact - .timestamp - .as_ref() - .map_or(0, |&t| min(t, smeared_time(context))); - let aheader = Aheader { - addr: contact.addr.clone(), - public_key, - prefer_encrypt: EncryptPreference::Mutual, - }; - let peerstate = match Peerstate::from_addr(context, &aheader.addr).await { - Err(e) => { - warn!( - context, - "import_vcard_contact: Cannot create peerstate from {}: {e:#}.", contact.addr - ); - return Ok(id); - } - Ok(p) => p, - }; - let peerstate = if let Some(mut p) = peerstate { - p.apply_gossip(&aheader, timestamp); - p - } else { - Peerstate::from_gossip(&aheader, timestamp) - }; - if let Err(e) = peerstate.save_to_db(&context.sql).await { - warn!( - context, - "import_vcard_contact: Could not save peerstate for {}: {e:#}.", contact.addr - ); - return Ok(id); - } - if let Err(e) = peerstate - .handle_fingerprint_change(context, timestamp) - .await - { - warn!( - context, - "import_vcard_contact: handle_fingerprint_change() failed for {}: {e:#}.", - contact.addr - ); - return Ok(id); - } - } if modified != Modifier::Created { return Ok(id); } @@ -1304,18 +1259,12 @@ impl Contact { .get_config(Config::ConfiguredAddr) .await? .unwrap_or_default(); - let peerstate = Peerstate::from_addr(context, &contact.addr).await?; - let Some(peerstate) = peerstate.filter(|peerstate| peerstate.peek_key(false).is_some()) - else { + let Some(fingerprint_other) = contact.fingerprint() else { return Ok(stock_str::encr_none(context).await); }; - let stock_message = match peerstate.prefer_encrypt { - EncryptPreference::Mutual => stock_str::e2e_preferred(context).await, - EncryptPreference::NoPreference => stock_str::e2e_available(context).await, - EncryptPreference::Reset => stock_str::encr_none(context).await, - }; + let stock_message = stock_str::e2e_available(context).await; let finger_prints = stock_str::finger_prints(context).await; let mut ret = format!("{stock_message}.\n{finger_prints}:"); @@ -1324,43 +1273,31 @@ impl Contact { .await? .dc_fingerprint() .to_string(); - let fingerprint_other_verified = peerstate - .peek_key(true) - .map(|k| k.dc_fingerprint().to_string()) - .unwrap_or_default(); - let fingerprint_other_unverified = peerstate - .peek_key(false) - .map(|k| k.dc_fingerprint().to_string()) - .unwrap_or_default(); - if addr < peerstate.addr { + if addr < contact.addr { cat_fingerprint( &mut ret, &stock_str::self_msg(context).await, &addr, &fingerprint_self, - "", ); cat_fingerprint( &mut ret, contact.get_display_name(), - &peerstate.addr, - &fingerprint_other_verified, - &fingerprint_other_unverified, + &contact.addr, + &fingerprint_other, ); } else { cat_fingerprint( &mut ret, contact.get_display_name(), - &peerstate.addr, - &fingerprint_other_verified, - &fingerprint_other_unverified, + &contact.addr, + &fingerprint_other, ); cat_fingerprint( &mut ret, &stock_str::self_msg(context).await, &addr, &fingerprint_self, - "", ); } @@ -1453,6 +1390,10 @@ impl Contact { /// It is possible for a PGP-contact to not have a certificate, /// e.g. if only the fingerprint is known from a QR-code. pub async fn openpgp_certificate(&self, context: &Context) -> Result> { + if self.id == ContactId::SELF { + return Ok(Some(load_self_public_key(context).await?)); + } + if let Some(fingerprint) = &self.fingerprint { if let Some(certificate_bytes) = context .sql @@ -1573,25 +1514,21 @@ impl Contact { /// Returns whether end-to-end encryption to the contact is available. pub async fn e2ee_avail(&self, context: &Context) -> Result { if self.id == ContactId::SELF { + // We don't need to check if we have our own key. return Ok(true); } - let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? else { - return Ok(false); - }; - Ok(peerstate.peek_key(false).is_some()) + Ok(self.openpgp_certificate(context).await?.is_some()) } /// Returns true if the contact - /// can be added to verified chats, - /// i.e. has a verified key - /// and Autocrypt key matches the verified key. + /// can be added to verified chats. /// /// If contact is verified /// UI should display green checkmark after the contact name /// in contact list items and /// in chat member list items. /// - /// In contact profile view, us this function only if there is no chat with the contact, + /// In contact profile view, use this function only if there is no chat with the contact, /// otherwise use is_chat_protected(). /// Use [Self::get_verifier_id] to display the verifier contact /// in the info section of the contact profile. @@ -1602,30 +1539,7 @@ impl Contact { return Ok(true); } - let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? else { - return Ok(false); - }; - - let forward_verified = peerstate.is_using_verified_key(); - let backward_verified = peerstate.is_backward_verified(context).await?; - Ok(forward_verified && backward_verified) - } - - /// Returns true if we have a verified key for the contact - /// and it is the same as Autocrypt key. - /// This is enough to send messages to the contact in verified chat - /// and verify received messages, but not enough to display green checkmark - /// or add the contact to verified groups. - pub async fn is_forward_verified(&self, context: &Context) -> Result { - if self.id == ContactId::SELF { - return Ok(true); - } - - let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? else { - return Ok(false); - }; - - Ok(peerstate.is_using_verified_key()) + Ok(self.get_verifier_id(context).await?.is_some()) } /// Returns the `ContactId` that verified the contact. @@ -1634,32 +1548,15 @@ impl Contact { /// display green checkmark in the profile and "Introduced by ..." line /// with the name and address of the contact /// formatted by [Self::get_name_n_addr]. - /// - /// If this function returns a verifier, - /// this does not necessarily mean - /// you can add the contact to verified chats. - /// Use [Self::is_verified] to check - /// if a contact can be added to a verified chat instead. pub async fn get_verifier_id(&self, context: &Context) -> Result> { - let Some(verifier_addr) = Peerstate::from_addr(context, self.get_addr()) - .await? - .and_then(|peerstate| peerstate.get_verifier().map(|addr| addr.to_owned())) - else { - return Ok(None); - }; + let verifier_id: u32 = + context.sql.query_get_value("SELECT verifier FROM contacts WHERE id=?", (self.id,)).await?. + context("Contact does not exist")?; - if addr_cmp(&verifier_addr, &self.addr) { - // Contact is directly verified via QR code. - return Ok(Some(ContactId::SELF)); - } - - match Contact::lookup_id_by_addr(context, &verifier_addr, Origin::Unknown).await? { - Some(contact_id) => Ok(Some(contact_id)), - None => { - let addr = &self.addr; - warn!(context, "Could not lookup contact with address {verifier_addr} which introduced {addr}."); - Ok(None) - } + if verifier_id == 0 { + Ok(None) + } else { + Ok(Some(ContactId::new(verifier_id))) } } @@ -1962,25 +1859,14 @@ fn cat_fingerprint( ret: &mut String, name: &str, addr: &str, - fingerprint_verified: &str, - fingerprint_unverified: &str, + fingerprint: &str, ) { *ret += &format!( "\n\n{} ({}):\n{}", name, addr, - if !fingerprint_verified.is_empty() { - fingerprint_verified - } else { - fingerprint_unverified - }, + fingerprint ); - if !fingerprint_verified.is_empty() - && !fingerprint_unverified.is_empty() - && fingerprint_verified != fingerprint_unverified - { - *ret += &format!("\n\n{name} (alternative):\n{fingerprint_unverified}"); - } } fn split_address_book(book: &str) -> Vec<(&str, &str)> { diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 6ddd00689c..83392c7147 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -29,7 +29,6 @@ use crate::message::{self, Message, MsgId, Viewtype}; use crate::mimeparser::{is_hidden, SystemMessage}; use crate::param::Param; use crate::peer_channels::create_iroh_header; -use crate::peerstate::Peerstate; use crate::simplify::escape_message_footer_marks; use crate::stock_str; use crate::tools::IsNoneOrEmpty; diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index dd5c9a105d..98e5b39b71 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1226,6 +1226,11 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint); if dbversion < migration_version { sql.execute_migration( "ALTER TABLE contacts ADD COLUMN fingerprint TEXT NOT NULL DEFAULT ''; + + -- Verifier is an ID of the verifier contact. + -- 0 if the contact is not verified. + ALTER TABLE contacts ADD COLUMN verifier INTEGER NOT NULL DEFAULT 0; + CREATE INDEX contacts_fingerprint_index ON contacts (fingerprint); CREATE TABLE public_keys ( From d1667f3476f4545dca7c5eaf0e6ffcf7340c5882 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 11 Apr 2025 21:03:51 +0000 Subject: [PATCH 027/381] no peerstates in REPL --- deltachat-repl/src/cmdline.rs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/deltachat-repl/src/cmdline.rs b/deltachat-repl/src/cmdline.rs index 0c51e3f31a..36dbd386ac 100644 --- a/deltachat-repl/src/cmdline.rs +++ b/deltachat-repl/src/cmdline.rs @@ -35,14 +35,6 @@ use tokio::fs; /// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4. async fn reset_tables(context: &Context, bits: i32) { println!("Resetting tables ({bits})..."); - if 0 != bits & 2 { - context - .sql() - .execute("DELETE FROM acpeerstates;", ()) - .await - .unwrap(); - println!("(2) Peerstates reset."); - } if 0 != bits & 4 { context .sql() @@ -296,15 +288,6 @@ async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<() verified_str, if !addr.is_empty() { addr } else { "addr unset" } ); - let peerstate = Peerstate::from_addr(context, addr) - .await - .expect("peerstate error"); - if peerstate.is_some() && *contact_id != ContactId::SELF { - line2 = format!( - ", prefer-encrypt={}", - peerstate.as_ref().unwrap().prefer_encrypt - ); - } println!("Contact#{}: {}{}", *contact_id, line, line2); } From bd31437b353d7b8fde4fd67796c3fca100455068 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 12 Apr 2025 01:25:03 +0000 Subject: [PATCH 028/381] mark as verified --- src/contact.rs | 43 +++++++++++++----------------------- src/contact/contact_tests.rs | 1 + src/peerstate.rs | 11 --------- src/receive_imf.rs | 5 ++++- 4 files changed, 20 insertions(+), 40 deletions(-) diff --git a/src/contact.rs b/src/contact.rs index 2ff725e570..597e835c5a 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -350,19 +350,14 @@ async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Resu fingerprint = String::new(); } - let (id, modified) = match Contact::add_or_lookup_ex( - context, - &contact.authname, - &addr, - &fingerprint, - origin, - ) - .await - { - Err(e) => return Err(e).context("Contact::add_or_lookup() failed"), - Ok((ContactId::SELF, _)) => return Ok(ContactId::SELF), - Ok(val) => val, - }; + let (id, modified) = + match Contact::add_or_lookup_ex(context, &contact.authname, &addr, &fingerprint, origin) + .await + { + Err(e) => return Err(e).context("Contact::add_or_lookup() failed"), + Ok((ContactId::SELF, _)) => return Ok(ContactId::SELF), + Ok(val) => val, + }; if modified != Modifier::None { context.emit_event(EventType::ContactsChanged(Some(id))); } @@ -1549,9 +1544,11 @@ impl Contact { /// with the name and address of the contact /// formatted by [Self::get_name_n_addr]. pub async fn get_verifier_id(&self, context: &Context) -> Result> { - let verifier_id: u32 = - context.sql.query_get_value("SELECT verifier FROM contacts WHERE id=?", (self.id,)).await?. - context("Contact does not exist")?; + let verifier_id: u32 = context + .sql + .query_get_value("SELECT verifier FROM contacts WHERE id=?", (self.id,)) + .await? + .context("Contact does not exist")?; if verifier_id == 0 { Ok(None) @@ -1855,18 +1852,8 @@ pub(crate) async fn update_last_seen( Ok(()) } -fn cat_fingerprint( - ret: &mut String, - name: &str, - addr: &str, - fingerprint: &str, -) { - *ret += &format!( - "\n\n{} ({}):\n{}", - name, - addr, - fingerprint - ); +fn cat_fingerprint(ret: &mut String, name: &str, addr: &str, fingerprint: &str) { + *ret += &format!("\n\n{} ({}):\n{}", name, addr, fingerprint); } fn split_address_book(book: &str) -> Vec<(&str, &str)> { diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs index e7843c16ee..27e73fe2fd 100644 --- a/src/contact/contact_tests.rs +++ b/src/contact/contact_tests.rs @@ -3,6 +3,7 @@ use deltachat_contact_tools::may_be_valid_addr; use super::*; use crate::chat::{get_chat_contacts, send_text_msg, Chat}; use crate::chatlist::Chatlist; +use crate::peerstate::Peerstate; use crate::receive_imf::receive_imf; use crate::test_utils::{self, TestContext, TestContextManager, TimeShiftFalsePositiveNote}; diff --git a/src/peerstate.rs b/src/peerstate.rs index e2f1840b21..6d145ae6c4 100644 --- a/src/peerstate.rs +++ b/src/peerstate.rs @@ -452,17 +452,6 @@ impl Peerstate { verified.is_some() && verified == self.peek_key_fingerprint(false) } - pub(crate) async fn is_backward_verified(&self, context: &Context) -> Result { - let Some(backward_verified_key_id) = self.backward_verified_key_id else { - return Ok(false); - }; - - let self_key_id = context.get_config_i64(Config::KeyId).await?; - - let backward_verified = backward_verified_key_id == self_key_id; - Ok(backward_verified) - } - /// Set this peerstate to verified; /// make sure to call `self.save_to_db` to save these changes. /// diff --git a/src/receive_imf.rs b/src/receive_imf.rs index d26162475f..ec5a05c177 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -3143,7 +3143,10 @@ async fn mark_recipients_as_verified( continue; } - // TODO: mark recipient PGP-contacts as verified. + context + .sql + .execute("UPDATE contacts SET verifier=? WHERE id=?", (from_id, id)) + .await?; } Ok(()) From bbeedeff444f367daf0a1b2fc2d1a18e2761d88a Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 12 Apr 2025 01:54:32 +0000 Subject: [PATCH 029/381] no should_encrypt --- src/e2ee.rs | 40 ---------------------------------------- src/qr.rs | 12 +++++++----- 2 files changed, 7 insertions(+), 45 deletions(-) diff --git a/src/e2ee.rs b/src/e2ee.rs index 315832974f..7bf54bbf4f 100644 --- a/src/e2ee.rs +++ b/src/e2ee.rs @@ -42,26 +42,6 @@ impl EncryptHelper { Aheader::new(addr, pk, self.prefer_encrypt) } - /// Determines if we can and should encrypt. - pub(crate) async fn should_encrypt( - &self, - context: &Context, - peerstates: &[(Option, String)], - ) -> Result { - let is_chatmail = context.is_chatmail().await?; - for (peerstate, _addr) in peerstates { - if let Some(peerstate) = peerstate { - // For chatmail we ignore the encryption preference, - // because we can either send encrypted or not at all. - if is_chatmail || peerstate.prefer_encrypt != EncryptPreference::Reset { - continue; - } - } - return Ok(false); - } - Ok(true) - } - /// Tries to encrypt the passed in `mail`. pub async fn encrypt( self, @@ -260,26 +240,6 @@ Sent with my Delta Chat Messenger: https://delta.chat"; vec![(Some(peerstate), addr.to_string())] } - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_should_encrypt() -> Result<()> { - let t = TestContext::new_alice().await; - let encrypt_helper = EncryptHelper::new(&t).await.unwrap(); - - let ps = new_peerstates(EncryptPreference::NoPreference); - assert!(encrypt_helper.should_encrypt(&t, &ps).await?); - - let ps = new_peerstates(EncryptPreference::Reset); - assert!(!encrypt_helper.should_encrypt(&t, &ps).await?); - - let ps = new_peerstates(EncryptPreference::Mutual); - assert!(encrypt_helper.should_encrypt(&t, &ps).await?); - - // test with missing peerstate - let ps = vec![(None, "bob@foo.bar".to_string())]; - assert!(!encrypt_helper.should_encrypt(&t, &ps).await?); - Ok(()) - } - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_chatmail_can_send_unencrypted() -> Result<()> { let mut tcm = TestContextManager::new(); diff --git a/src/qr.rs b/src/qr.rs index b8ee21d030..f3c101054a 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -457,11 +457,6 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result { None }; - // retrieve known state for this fingerprint - let peerstate = Peerstate::from_fingerprint(context, &fingerprint) - .await - .context("Can't load peerstate")?; - if let (Some(addr), Some(invitenumber), Some(authcode)) = (&addr, invitenumber, authcode) { let addr = ContactAddress::new(addr)?; let (contact_id, _) = Contact::add_or_lookup_ex( @@ -534,6 +529,13 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result { }) } } else if let Some(addr) = addr { + // FIXME don't use peerstate + + // retrieve known state for this fingerprint + let peerstate = Peerstate::from_fingerprint(context, &fingerprint) + .await + .context("Can't load peerstate")?; + if let Some(peerstate) = peerstate { let peerstate_addr = ContactAddress::new(&peerstate.addr)?; let (contact_id, _) = From b81fd3ee8193169848edb28164da9fa987cce2a8 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 12 Apr 2025 02:07:26 +0000 Subject: [PATCH 030/381] unused --- src/chat.rs | 16 +++++----------- src/e2ee.rs | 32 +++----------------------------- src/mimeparser.rs | 17 ++--------------- src/receive_imf.rs | 1 - 4 files changed, 10 insertions(+), 56 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index 4ae93608bc..7d0b7e2665 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1334,17 +1334,11 @@ impl ChatId { { let contact = Contact::get_by_id(context, *contact_id).await?; let addr = contact.get_addr(); - let peerstate = Peerstate::from_addr(context, addr).await?; - - match peerstate - .filter(|peerstate| peerstate.peek_key(false).is_some()) - .map(|peerstate| peerstate.prefer_encrypt) - { - Some(EncryptPreference::Mutual) | Some(EncryptPreference::NoPreference) => { - ret_available += &format!("{addr}\n") - } - Some(EncryptPreference::Reset) | None => ret_reset += &format!("{addr}\n"), - }; + if contact.is_pgp_contact() { + ret_available += &format!("{addr}\n"); + } else { + ret_reset += &format!("{addr}\n"); + } } let mut ret = String::new(); diff --git a/src/e2ee.rs b/src/e2ee.rs index 7bf54bbf4f..b95fe7b71f 100644 --- a/src/e2ee.rs +++ b/src/e2ee.rs @@ -3,7 +3,7 @@ use std::collections::BTreeSet; use std::io::Cursor; -use anyhow::{bail, Result}; +use anyhow::Result; use mail_builder::mime::MimePart; use num_traits::FromPrimitive; @@ -11,7 +11,6 @@ use crate::aheader::{Aheader, EncryptPreference}; use crate::config::Config; use crate::context::Context; use crate::key::{load_self_public_key, load_self_secret_key, SignedPublicKey}; -use crate::peerstate::Peerstate; use crate::pgp; #[derive(Debug)] @@ -90,11 +89,11 @@ mod tests { use super::*; use crate::chat::send_text_msg; use crate::config::Config; - use crate::key::DcKey; use crate::message::{Message, Viewtype}; use crate::param::Param; use crate::receive_imf::receive_imf; - use crate::test_utils::{bob_keypair, TestContext, TestContextManager}; + use crate::test_utils::{TestContext, TestContextManager}; + use crate::peerstate::Peerstate; mod ensure_secret_key_exists { use super::*; @@ -215,31 +214,6 @@ Sent with my Delta Chat Messenger: https://delta.chat"; Ok(()) } - fn new_peerstates(prefer_encrypt: EncryptPreference) -> Vec<(Option, String)> { - let addr = "bob@foo.bar"; - let pub_key = bob_keypair().public; - let peerstate = Peerstate { - addr: addr.into(), - last_seen: 13, - last_seen_autocrypt: 14, - prefer_encrypt, - public_key: Some(pub_key.clone()), - public_key_fingerprint: Some(pub_key.dc_fingerprint()), - gossip_key: Some(pub_key.clone()), - gossip_timestamp: 15, - gossip_key_fingerprint: Some(pub_key.dc_fingerprint()), - verified_key: Some(pub_key.clone()), - verified_key_fingerprint: Some(pub_key.dc_fingerprint()), - verifier: None, - secondary_verified_key: None, - secondary_verified_key_fingerprint: None, - secondary_verifier: None, - backward_verified_key_id: None, - fingerprint_changed: false, - }; - vec![(Some(peerstate), addr.to_string())] - } - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_chatmail_can_send_unencrypted() -> Result<()> { let mut tcm = TestContextManager::new(); diff --git a/src/mimeparser.rs b/src/mimeparser.rs index e581546429..ccfc462b37 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -399,15 +399,6 @@ impl MimeMessage { None }; - // The peerstate that will be used to validate the signatures. - let mut peerstate = get_autocrypt_peerstate( - context, - &from.addr, - autocrypt_header.as_ref(), - timestamp_sent, - ) - .await?; - let public_keyring = if incoming { if let Some(autocrypt_header) = autocrypt_header { vec![autocrypt_header.public_key] @@ -518,12 +509,6 @@ impl MimeMessage { if !encrypted { signatures.clear(); } - if let Some(peerstate) = &mut peerstate { - if peerstate.prefer_encrypt != EncryptPreference::Mutual && !signatures.is_empty() { - peerstate.prefer_encrypt = EncryptPreference::Mutual; - peerstate.save_to_db(&context.sql).await?; - } - } let mut parser = MimeMessage { parts: Vec::new(), @@ -1322,6 +1307,8 @@ impl MimeMessage { if decoded_data.is_empty() { return Ok(()); } + + // Process attached PGP keys. if let Some(peerstate) = &mut self.peerstate { if peerstate.prefer_encrypt != EncryptPreference::Mutual && mime_type.type_() == mime::APPLICATION diff --git a/src/receive_imf.rs b/src/receive_imf.rs index ec5a05c177..6774215459 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -3137,7 +3137,6 @@ async fn mark_recipients_as_verified( if mimeparser.get_header(HeaderDef::ChatVerified).is_none() { return Ok(()); } - let contact = Contact::get_by_id(context, from_id).await?; for id in to_ids.iter().filter_map(|&x| x) { if id == ContactId::SELF { continue; From fe3594116f0882148cff778ccbe5a32259ff9517 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 12 Apr 2025 02:25:39 +0000 Subject: [PATCH 031/381] less peerstate --- src/contact.rs | 3 +- src/decrypt.rs | 2 +- src/mimeparser.rs | 17 ++--- src/receive_imf.rs | 98 ++-------------------------- src/receive_imf/receive_imf_tests.rs | 1 + src/securejoin.rs | 11 ---- 6 files changed, 17 insertions(+), 115 deletions(-) diff --git a/src/contact.rs b/src/contact.rs index 597e835c5a..1f8e81f28f 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -20,7 +20,6 @@ use serde::{Deserialize, Serialize}; use tokio::task; use tokio::time::{timeout, Duration}; -use crate::aheader::{Aheader, EncryptPreference}; use crate::blob::BlobObject; use crate::chat::{ChatId, ChatIdBlocked, ProtectionStatus}; use crate::color::str_to_color; @@ -34,7 +33,7 @@ use crate::message::MessageState; use crate::mimeparser::AvatarAction; use crate::param::{Param, Params}; use crate::sync::{self, Sync::*}; -use crate::tools::{duration_to_str, get_abs_path, smeared_time, time, SystemTime}; +use crate::tools::{duration_to_str, get_abs_path, time, SystemTime}; use crate::{chat, chatlist_events, stock_str}; /// Time during which a contact is considered as seen recently. diff --git a/src/decrypt.rs b/src/decrypt.rs index 3774f97437..7886f7f675 100644 --- a/src/decrypt.rs +++ b/src/decrypt.rs @@ -8,7 +8,7 @@ use mailparse::ParsedMail; use crate::aheader::Aheader; use crate::context::Context; -use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey}; +use crate::key::{Fingerprint, SignedPublicKey, SignedSecretKey}; use crate::peerstate::Peerstate; use crate::pgp; diff --git a/src/mimeparser.rs b/src/mimeparser.rs index ccfc462b37..ca721fa028 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -77,7 +77,6 @@ pub(crate) struct MimeMessage { /// messages to this address to post them to the list. pub list_post: Option, pub chat_disposition_notification_to: Option, - pub peerstate: Option, pub decrypting_failed: bool, /// Set of valid signature fingerprints if a message is an @@ -522,7 +521,6 @@ impl MimeMessage { from, incoming, chat_disposition_notification_to, - peerstate, decrypting_failed: mail.is_err(), // only non-empty if it was a valid autocrypt message @@ -1309,15 +1307,14 @@ impl MimeMessage { } // Process attached PGP keys. - if let Some(peerstate) = &mut self.peerstate { - if peerstate.prefer_encrypt != EncryptPreference::Mutual - && mime_type.type_() == mime::APPLICATION - && mime_type.subtype().as_str() == "pgp-keys" - && Self::try_set_peer_key_from_file_part(context, peerstate, decoded_data).await? - { - return Ok(()); - } + /* + if mime_type.type_() == mime::APPLICATION + && mime_type.subtype().as_str() == "pgp-keys" + && Self::try_set_peer_key_from_file_part(context, peerstate, decoded_data).await? + { + return Ok(()); } + */ let mut part = Part::default(); let msg_type = if context .is_webxdc_file(filename, decoded_data) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 6774215459..e06e9e3ad1 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -32,7 +32,6 @@ use crate::message::{ use crate::mimeparser::{parse_message_ids, AvatarAction, MimeMessage, SystemMessage}; use crate::param::{Param, Params}; use crate::peer_channels::{add_gossip_peer_from_header, insert_topic_stub}; -use crate::peerstate::Peerstate; use crate::reaction::{set_msg_reaction, Reaction}; use crate::rusqlite::OptionalExtension; use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device}; @@ -382,8 +381,6 @@ pub(crate) async fn receive_imf_inner( .await?; }; - update_verified_keys(context, &mut mime_parser, from_id).await?; - let received_msg; if mime_parser.get_header(HeaderDef::SecureJoin).is_some() { let res; @@ -391,10 +388,6 @@ pub(crate) async fn receive_imf_inner( res = handle_securejoin_handshake(context, &mut mime_parser, from_id) .await .context("error in Secure-Join message handling")?; - - // Peerstate could be updated by handling the Securejoin handshake. - let contact = Contact::get_by_id(context, from_id).await?; - mime_parser.peerstate = Peerstate::from_addr(context, contact.get_addr()).await?; } else { let to_id = to_ids.first().copied().flatten().unwrap_or(ContactId::SELF); // handshake may mark contacts as verified and must be processed before chats are created @@ -429,24 +422,6 @@ pub(crate) async fn receive_imf_inner( mark_recipients_as_verified(context, from_id, &to_ids, &mime_parser).await?; } - if verified_encryption == VerifiedEncryption::Verified - && mime_parser.get_header(HeaderDef::ChatVerified).is_some() - { - if let Some(peerstate) = &mut mime_parser.peerstate { - // NOTE: it might be better to remember ID of the key - // that we used to decrypt the message, but - // it is unlikely that default key ever changes - // as it only happens when user imports a new default key. - // - // Backward verification is not security-critical, - // it is only needed to avoid adding user who does not - // have our key as verified to protected chats. - peerstate.backward_verified_key_id = - Some(context.get_config_i64(Config::KeyId).await?).filter(|&id| id > 0); - peerstate.save_to_db(&context.sql).await?; - } - } - let received_msg = if let Some(received_msg) = received_msg { received_msg } else { @@ -1054,13 +1029,10 @@ async fn add_parts( ) .await?; } - if let Some(peerstate) = &mime_parser.peerstate { - restore_protection = new_protection != ProtectionStatus::Protected - && peerstate.prefer_encrypt == EncryptPreference::Mutual - // Check that the contact still has the Autocrypt key same as the - // verified key, see also `Peerstate::is_using_verified_key()`. - && contact.is_verified(context).await?; - } + restore_protection = new_protection != ProtectionStatus::Protected + // Check that the contact still has the Autocrypt key same as the + // verified key, see also `Peerstate::is_using_verified_key()`. + && contact.is_verified(context).await?; } } } @@ -3028,65 +3000,6 @@ enum VerifiedEncryption { NotVerified(String), // The string contains the reason why it's not verified } -/// Moves secondary verified key to primary verified key -/// if the message is signed with a secondary verified key. -/// Removes secondary verified key if the message is signed with primary key. -async fn update_verified_keys( - context: &Context, - mimeparser: &mut MimeMessage, - from_id: ContactId, -) -> Result> { - if from_id == ContactId::SELF { - return Ok(None); - } - - if !mimeparser.was_encrypted() { - return Ok(None); - } - - let Some(peerstate) = &mut mimeparser.peerstate else { - // No peerstate means no verified keys. - return Ok(None); - }; - - let signed_with_primary_verified_key = peerstate - .verified_key_fingerprint - .as_ref() - .filter(|fp| mimeparser.signatures.contains(fp)) - .is_some(); - let signed_with_secondary_verified_key = peerstate - .secondary_verified_key_fingerprint - .as_ref() - .filter(|fp| mimeparser.signatures.contains(fp)) - .is_some(); - - if signed_with_primary_verified_key { - // Remove secondary key if it exists. - if peerstate.secondary_verified_key.is_some() - || peerstate.secondary_verified_key_fingerprint.is_some() - || peerstate.secondary_verifier.is_some() - { - peerstate.secondary_verified_key = None; - peerstate.secondary_verified_key_fingerprint = None; - peerstate.secondary_verifier = None; - peerstate.save_to_db(&context.sql).await?; - } - - // No need to notify about secondary key removal. - Ok(None) - } else if signed_with_secondary_verified_key { - peerstate.verified_key = peerstate.secondary_verified_key.take(); - peerstate.verified_key_fingerprint = peerstate.secondary_verified_key_fingerprint.take(); - peerstate.verifier = peerstate.secondary_verifier.take(); - peerstate.fingerprint_changed = true; - peerstate.save_to_db(&context.sql).await?; - - // Primary verified key changed. - Ok(None) - } else { - Ok(None) - } -} /// Checks whether the message is allowed to appear in a protected chat. /// @@ -3106,6 +3019,8 @@ fn has_verified_encryption( // this check is skipped for SELF as there is no proper SELF-peerstate // and results in group-splits otherwise. if from_id != ContactId::SELF { + // FIXME + /* let Some(peerstate) = &mimeparser.peerstate else { return Ok(NotVerified( "No peerstate, the contact isn't verified".to_string(), @@ -3123,6 +3038,7 @@ fn has_verified_encryption( "The message was sent with non-verified encryption".to_string(), )); } + */ } Ok(Verified) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 72cfc2fc81..2360800258 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -13,6 +13,7 @@ use crate::chatlist::Chatlist; use crate::constants::{DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS}; use crate::contact; use crate::download::MIN_DOWNLOAD_LIMIT; +use crate::peerstate::Peerstate; use crate::imap::prefetch_should_download; use crate::imex::{imex, ImexMode}; use crate::securejoin::get_securejoin_qr; diff --git a/src/securejoin.rs b/src/securejoin.rs index 8f81317b06..faeebec6de 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -499,17 +499,6 @@ pub(crate) async fn handle_securejoin_handshake( return Ok(HandshakeMessage::Propagate); } - // Mark peer as backward verified. - // - // This is needed for the case when we join a non-protected group - // because in this case `Chat-Verified` header that otherwise - // sets backward verification is not sent. - if let Some(peerstate) = &mut mime_message.peerstate { - peerstate.backward_verified_key_id = - Some(context.get_config_i64(Config::KeyId).await?).filter(|&id| id > 0); - peerstate.save_to_db(&context.sql).await?; - } - context.emit_event(EventType::SecurejoinJoinerProgress { contact_id, progress: JoinerProgress::Succeeded.to_usize(), From ef17d72ccf72c916e0efe7311cc3750d42623ca8 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 12 Apr 2025 02:47:06 +0000 Subject: [PATCH 032/381] process Autocrypt --- src/contact/contact_tests.rs | 9 ++---- src/decrypt.rs | 46 ---------------------------- src/mimeparser.rs | 11 ++++++- src/receive_imf.rs | 1 - src/receive_imf/receive_imf_tests.rs | 1 + 5 files changed, 13 insertions(+), 55 deletions(-) diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs index 27e73fe2fd..e09848d02c 100644 --- a/src/contact/contact_tests.rs +++ b/src/contact/contact_tests.rs @@ -3,7 +3,6 @@ use deltachat_contact_tools::may_be_valid_addr; use super::*; use crate::chat::{get_chat_contacts, send_text_msg, Chat}; use crate::chatlist::Chatlist; -use crate::peerstate::Peerstate; use crate::receive_imf::receive_imf; use crate::test_utils::{self, TestContext, TestContextManager, TimeShiftFalsePositiveNote}; @@ -1065,12 +1064,8 @@ async fn test_make_n_import_vcard() -> Result<()> { let chat = bob.create_chat(alice).await; let sent_msg = bob.send_text(chat.id, "moin").await; let bob_id = alice.recv_msg(&sent_msg).await.from_id; - let key_base64 = Peerstate::from_addr(alice, &bob_addr) - .await? - .unwrap() - .peek_key(false) - .unwrap() - .to_base64(); + let bob_contact = Contact::get_by_id(alice, bob_id).await?; + let key_base64 = bob_contact.openpgp_certificate(alice).await?.unwrap().to_base64(); let fiona_id = Contact::create(alice, "Fiona", "fiona@example.net").await?; assert_eq!(make_vcard(alice, &[]).await?, "".to_string()); diff --git a/src/decrypt.rs b/src/decrypt.rs index 7886f7f675..d6bf2b8f5b 100644 --- a/src/decrypt.rs +++ b/src/decrypt.rs @@ -143,52 +143,6 @@ pub(crate) fn validate_detached_signature<'a, 'b>( } } -/// Applies Autocrypt header to Autocrypt peer state and saves it into the database. -/// -/// Returns updated peerstate. -pub(crate) async fn get_autocrypt_peerstate( - context: &Context, - from: &str, - autocrypt_header: Option<&Aheader>, - message_time: i64, -) -> Result> { - let allow_change = !context.is_self_addr(from).await?; - let mut peerstate; - - // Apply Autocrypt header - if let Some(header) = autocrypt_header { - peerstate = Peerstate::from_addr(context, from).await?; - - if let Some(ref mut peerstate) = peerstate { - if addr_cmp(&peerstate.addr, from) { - if allow_change { - peerstate.apply_header(context, header, message_time); - peerstate.save_to_db(&context.sql).await?; - } else { - info!( - context, - "Refusing to update existing peerstate of {}", &peerstate.addr - ); - } - } - // If `peerstate.addr` and `from` differ, this means that - // someone is using the same key but a different addr, probably - // because they made an AEAP transition. - // But we don't know if that's legit until we checked the - // signatures, so wait until then with writing anything - // to the database. - } else { - let p = Peerstate::from_header(header, message_time); - p.save_to_db(&context.sql).await?; - peerstate = Some(p); - } - } else { - peerstate = Peerstate::from_addr(context, from).await?; - } - - Ok(peerstate) -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/mimeparser.rs b/src/mimeparser.rs index ca721fa028..f8f4080307 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -22,7 +22,7 @@ use crate::constants; use crate::contact::ContactId; use crate::context::Context; use crate::decrypt::{ - get_autocrypt_peerstate, get_encrypted_mime, try_decrypt, validate_detached_signature, + get_encrypted_mime, try_decrypt, validate_detached_signature, }; use crate::dehtml::dehtml; use crate::events::EventType; @@ -398,6 +398,15 @@ impl MimeMessage { None }; + if let Some(autocrypt_header) = &autocrypt_header { + let fingerprint = autocrypt_header.public_key.dc_fingerprint().hex(); + context.sql.execute("INSERT INTO public_keys (fingerprint, public_key) + VALUES (?, ?) + ON CONFLICT (fingerprint) + DO NOTHING", + (&fingerprint, autocrypt_header.public_key.to_bytes())).await?; + } + let public_keyring = if incoming { if let Some(autocrypt_header) = autocrypt_header { vec![autocrypt_header.public_key] diff --git a/src/receive_imf.rs b/src/receive_imf.rs index e06e9e3ad1..db6fdc68d4 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -12,7 +12,6 @@ use mailparse::SingleInfo; use num_traits::FromPrimitive; use regex::Regex; -use crate::aheader::EncryptPreference; use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus}; use crate::config::Config; use crate::constants::{Blocked, Chattype, ShowEmails, DC_CHAT_ID_TRASH, EDITED_PREFIX}; diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 2360800258..e57b8b9a55 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -9,6 +9,7 @@ use crate::chat::{ get_chat_msgs, is_contact_in_chat, remove_contact_from_chat, send_text_msg, ChatItem, ChatVisibility, }; +use crate::aheader::EncryptPreference; use crate::chatlist::Chatlist; use crate::constants::{DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS}; use crate::contact; From 8121e389b0825e684d04cd8bc23e5494dca3c90d Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 12 Apr 2025 04:41:55 +0000 Subject: [PATCH 033/381] format and logs --- src/contact/contact_tests.rs | 6 +++++- src/e2ee.rs | 2 +- src/mimefactory.rs | 3 +++ src/mimeparser.rs | 19 ++++++++++++++----- src/receive_imf/receive_imf_tests.rs | 4 ++-- 5 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs index e09848d02c..ffc1c26a4c 100644 --- a/src/contact/contact_tests.rs +++ b/src/contact/contact_tests.rs @@ -1065,7 +1065,11 @@ async fn test_make_n_import_vcard() -> Result<()> { let sent_msg = bob.send_text(chat.id, "moin").await; let bob_id = alice.recv_msg(&sent_msg).await.from_id; let bob_contact = Contact::get_by_id(alice, bob_id).await?; - let key_base64 = bob_contact.openpgp_certificate(alice).await?.unwrap().to_base64(); + let key_base64 = bob_contact + .openpgp_certificate(alice) + .await? + .unwrap() + .to_base64(); let fiona_id = Contact::create(alice, "Fiona", "fiona@example.net").await?; assert_eq!(make_vcard(alice, &[]).await?, "".to_string()); diff --git a/src/e2ee.rs b/src/e2ee.rs index b95fe7b71f..138f096bb0 100644 --- a/src/e2ee.rs +++ b/src/e2ee.rs @@ -91,9 +91,9 @@ mod tests { use crate::config::Config; use crate::message::{Message, Viewtype}; use crate::param::Param; + use crate::peerstate::Peerstate; use crate::receive_imf::receive_imf; use crate::test_utils::{TestContext, TestContextManager}; - use crate::peerstate::Peerstate; mod ensure_secret_key_exists { use super::*; diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 83392c7147..66e1b1d196 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -317,6 +317,9 @@ impl MimeFactory { certificates.push((addr.clone(), certificate)) } else { missing_key_addresses.insert(addr.clone()); + if is_encrypted { + warn!(context, "Missing key for {addr}"); + } } } diff --git a/src/mimeparser.rs b/src/mimeparser.rs index f8f4080307..5913fa7c50 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -21,9 +21,7 @@ use crate::config::Config; use crate::constants; use crate::contact::ContactId; use crate::context::Context; -use crate::decrypt::{ - get_encrypted_mime, try_decrypt, validate_detached_signature, -}; +use crate::decrypt::{get_encrypted_mime, try_decrypt, validate_detached_signature}; use crate::dehtml::dehtml; use crate::events::EventType; use crate::headerdef::{HeaderDef, HeaderDefMap}; @@ -400,11 +398,22 @@ impl MimeMessage { if let Some(autocrypt_header) = &autocrypt_header { let fingerprint = autocrypt_header.public_key.dc_fingerprint().hex(); - context.sql.execute("INSERT INTO public_keys (fingerprint, public_key) + let inserted = context + .sql + .execute( + "INSERT INTO public_keys (fingerprint, public_key) VALUES (?, ?) ON CONFLICT (fingerprint) DO NOTHING", - (&fingerprint, autocrypt_header.public_key.to_bytes())).await?; + (&fingerprint, autocrypt_header.public_key.to_bytes()), + ) + .await?; + if inserted > 0 { + info!( + context, + "Saved key with fingerprint {fingerprint} from the Autocrypt header" + ); + } } let public_keyring = if incoming { diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index e57b8b9a55..58f66b889b 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -4,19 +4,19 @@ use std::time::Duration; use tokio::fs; use super::*; +use crate::aheader::EncryptPreference; use crate::chat::{ add_contact_to_chat, add_to_chat_contacts_table, create_group_chat, get_chat_contacts, get_chat_msgs, is_contact_in_chat, remove_contact_from_chat, send_text_msg, ChatItem, ChatVisibility, }; -use crate::aheader::EncryptPreference; use crate::chatlist::Chatlist; use crate::constants::{DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS}; use crate::contact; use crate::download::MIN_DOWNLOAD_LIMIT; -use crate::peerstate::Peerstate; use crate::imap::prefetch_should_download; use crate::imex::{imex, ImexMode}; +use crate::peerstate::Peerstate; use crate::securejoin::get_securejoin_qr; use crate::test_utils::{get_chat_msg, mark_as_verified, TestContext, TestContextManager}; use crate::tools::{time, SystemTime}; From 459429f1814863d8123b20695196178a2697738f Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 12 Apr 2025 04:41:55 +0000 Subject: [PATCH 034/381] Send vc-auth-required to the PGP-contact --- src/mimeparser.rs | 13 +++++++++++-- src/securejoin.rs | 13 ++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 5913fa7c50..623fde4cbd 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -88,6 +88,11 @@ pub(crate) struct MimeMessage { /// regardless of whether they modified any peerstates. pub gossiped_keys: HashMap, + /// Fingerprint of the key in the Autocrypt header. + /// + /// It is not verified that the sender can use this key. + pub autocrypt_fingerprint: Option, + /// True if the message is a forwarded message. pub is_forwarded: bool, pub is_system_message: SystemMessage, @@ -396,7 +401,7 @@ impl MimeMessage { None }; - if let Some(autocrypt_header) = &autocrypt_header { + let autocrypt_fingerprint = if let Some(autocrypt_header) = &autocrypt_header { let fingerprint = autocrypt_header.public_key.dc_fingerprint().hex(); let inserted = context .sql @@ -414,7 +419,10 @@ impl MimeMessage { "Saved key with fingerprint {fingerprint} from the Autocrypt header" ); } - } + Some(fingerprint) + } else { + None + }; let public_keyring = if incoming { if let Some(autocrypt_header) = autocrypt_header { @@ -543,6 +551,7 @@ impl MimeMessage { // only non-empty if it was a valid autocrypt message signatures, + autocrypt_fingerprint, gossiped_keys, is_forwarded: false, mdn_reports: Vec::new(), diff --git a/src/securejoin.rs b/src/securejoin.rs index faeebec6de..72964428ab 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -2,6 +2,7 @@ use anyhow::{ensure, Context as _, Error, Result}; use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; +use deltachat_contact_tools::ContactAddress; use crate::aheader::EncryptPreference; use crate::chat::{self, get_chat_id_by_grpid, Chat, ChatId, ChatIdBlocked, ProtectionStatus}; @@ -334,10 +335,20 @@ pub(crate) async fn handle_securejoin_handshake( inviter_progress(context, contact_id, 300); + let from_addr = ContactAddress::new(&mime_message.from.addr)?; + let autocrypt_fingerprint = mime_message.autocrypt_fingerprint.clone().unwrap_or_default(); + let (autocrypt_contact_id, _) = Contact::add_or_lookup_ex( + context, + "", + &from_addr, + &autocrypt_fingerprint, + Origin::IncomingUnknownFrom + ).await?; + // Alice -> Bob send_alice_handshake_msg( context, - contact_id, + autocrypt_contact_id, &format!("{}-auth-required", &step.get(..2).unwrap_or_default()), ) .await From de677c0a0c13f5b6feb2463f662b761c0e7c6801 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 12 Apr 2025 04:41:55 +0000 Subject: [PATCH 035/381] test --- src/receive_imf.rs | 1 - src/securejoin/securejoin_tests.rs | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index db6fdc68d4..23ac18cad1 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -2999,7 +2999,6 @@ enum VerifiedEncryption { NotVerified(String), // The string contains the reason why it's not verified } - /// Checks whether the message is allowed to appear in a protected chat. /// /// This means that it is encrypted and signed with a verified key. diff --git a/src/securejoin/securejoin_tests.rs b/src/securejoin/securejoin_tests.rs index ffb3f54c83..c08cf9c256 100644 --- a/src/securejoin/securejoin_tests.rs +++ b/src/securejoin/securejoin_tests.rs @@ -155,6 +155,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) { // Step 4: Bob receives vc-auth-required, sends vc-request-with-auth bob.recv_msg_trash(&sent).await; let bob_chat = bob.get_pgp_chat(&alice).await; + assert_eq!(bob_chat.why_cant_send(&bob).await.unwrap(), None); assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true); // Check Bob emitted the JoinerProgress event. From 452c8a3bf0c60cc090b2eccafa69b93e9ea456ff Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 12 Apr 2025 16:03:43 +0000 Subject: [PATCH 036/381] Fix verifying contacts by fingerprint --- src/securejoin.rs | 37 ++++++------------------------------- 1 file changed, 6 insertions(+), 31 deletions(-) diff --git a/src/securejoin.rs b/src/securejoin.rs index 72964428ab..ea67a12d62 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -199,45 +199,20 @@ async fn info_chat_id(context: &Context, contact_id: ContactId) -> Result Result { + let fingerprint = fingerprint.hex(); let contact = Contact::get_by_id(context, contact_id).await?; - let peerstate = match Peerstate::from_addr(context, contact.get_addr()).await { - Ok(peerstate) => peerstate, - Err(err) => { - warn!( - context, - "Failed to sender peerstate for {}: {}", - contact.get_addr(), - err - ); - return Ok(false); - } - }; - - if let Some(mut peerstate) = peerstate { - if peerstate - .public_key_fingerprint - .as_ref() - .filter(|&fp| fp == fingerprint) - .is_some() - { - if let Some(public_key) = &peerstate.public_key { - let verifier = contact.get_addr().to_owned(); - peerstate.set_verified(public_key.clone(), fingerprint.clone(), verifier)?; - peerstate.prefer_encrypt = EncryptPreference::Mutual; - peerstate.save_to_db(&context.sql).await?; - return Ok(true); - } - } + let is_verified = contact.fingerprint().is_some_and(|fp| fp == fingerprint); + if is_verified { + context.sql.execute("UPDATE contacts SET verifier=?1 WHERE id=?1", (contact_id,)).await?; } - - Ok(false) + Ok(is_verified) } /// What to do with a Secure-Join handshake message after it was handled. From 64a6b3df050c978e795afabbf02b84f1193f5981 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 12 Apr 2025 16:15:48 +0000 Subject: [PATCH 037/381] securejoin test passes --- src/securejoin.rs | 62 ++++++------------------------ src/securejoin/bob.rs | 9 ++++- src/securejoin/securejoin_tests.rs | 6 ++- 3 files changed, 24 insertions(+), 53 deletions(-) diff --git a/src/securejoin.rs b/src/securejoin.rs index ea67a12d62..39c97876de 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -1,8 +1,8 @@ //! Implementation of [SecureJoin protocols](https://securejoin.delta.chat/). use anyhow::{ensure, Context as _, Error, Result}; -use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; use deltachat_contact_tools::ContactAddress; +use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; use crate::aheader::EncryptPreference; use crate::chat::{self, get_chat_id_by_grpid, Chat, ChatId, ChatIdBlocked, ProtectionStatus}; @@ -210,7 +210,10 @@ async fn verify_sender_by_fingerprint( let contact = Contact::get_by_id(context, contact_id).await?; let is_verified = contact.fingerprint().is_some_and(|fp| fp == fingerprint); if is_verified { - context.sql.execute("UPDATE contacts SET verifier=?1 WHERE id=?1", (contact_id,)).await?; + context + .sql + .execute("UPDATE contacts SET verifier=?1 WHERE id=?1", (contact_id,)) + .await?; } Ok(is_verified) } @@ -311,14 +314,18 @@ pub(crate) async fn handle_securejoin_handshake( inviter_progress(context, contact_id, 300); let from_addr = ContactAddress::new(&mime_message.from.addr)?; - let autocrypt_fingerprint = mime_message.autocrypt_fingerprint.clone().unwrap_or_default(); + let autocrypt_fingerprint = mime_message + .autocrypt_fingerprint + .clone() + .unwrap_or_default(); let (autocrypt_contact_id, _) = Contact::add_or_lookup_ex( context, "", &from_addr, &autocrypt_fingerprint, - Origin::IncomingUnknownFrom - ).await?; + Origin::IncomingUnknownFrom, + ) + .await?; // Alice -> Bob send_alice_handshake_msg( @@ -394,26 +401,6 @@ pub(crate) async fn handle_securejoin_handshake( } }; - let contact_addr = Contact::get_by_id(context, contact_id) - .await? - .get_addr() - .to_owned(); - - let backward_verified = true; - let fingerprint_found = mark_peer_as_verified( - context, - fingerprint.clone(), - contact_addr, - backward_verified, - ) - .await?; - if !fingerprint_found { - warn!( - context, - "Ignoring {step} message because of the failure to find matching peerstate." - ); - return Ok(HandshakeMessage::Ignore); - } contact_id.regossip_keys(context).await?; ContactId::scaleup_origin(context, &[contact_id], Origin::SecurejoinInvited).await?; // for setup-contact, make Alice's one-to-one chat with Bob visible @@ -669,31 +656,6 @@ async fn could_not_establish_secure_connection( Ok(()) } -/// Tries to mark peer with provided key fingerprint as verified. -/// -/// Returns true if such key was found, false otherwise. -async fn mark_peer_as_verified( - context: &Context, - fingerprint: Fingerprint, - verifier: String, - backward_verified: bool, -) -> Result { - let Some(ref mut peerstate) = Peerstate::from_fingerprint(context, &fingerprint).await? else { - return Ok(false); - }; - let Some(ref public_key) = peerstate.public_key else { - return Ok(false); - }; - peerstate.set_verified(public_key.clone(), fingerprint, verifier)?; - peerstate.prefer_encrypt = EncryptPreference::Mutual; - if backward_verified { - peerstate.backward_verified_key_id = - Some(context.get_config_i64(Config::KeyId).await?).filter(|&id| id > 0); - } - peerstate.save_to_db(&context.sql).await?; - Ok(true) -} - /* ****************************************************************************** * Tools: Misc. ******************************************************************************/ diff --git a/src/securejoin/bob.rs b/src/securejoin/bob.rs index 60177808b5..4449a3dbac 100644 --- a/src/securejoin/bob.rs +++ b/src/securejoin/bob.rs @@ -59,8 +59,15 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul let peer_verified = verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id()) .await?; + let has_key = context + .sql + .exists( + "SELECT COUNT(*) FROM public_keys WHERE fingerprint=?", + (invite.fingerprint().hex(),), + ) + .await?; - if peer_verified { + if has_key && peer_verified { // The scanned fingerprint matches Alice's key, we can proceed to step 4b. info!(context, "Taking securejoin protocol shortcut"); send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::RequestWithAuth) diff --git a/src/securejoin/securejoin_tests.rs b/src/securejoin/securejoin_tests.rs index c08cf9c256..6f11e92027 100644 --- a/src/securejoin/securejoin_tests.rs +++ b/src/securejoin/securejoin_tests.rs @@ -294,8 +294,10 @@ async fn test_setup_contact_ex(case: SetupContactCase) { "vc-contact-confirm" ); - // Bob should not yet have Alice verified - assert_eq!(contact_alice.is_verified(&bob.ctx).await.unwrap(), false); + // Bob has verified Alice already. + // + // Alice may not have verified Bob yet. + assert_eq!(contact_alice.is_verified(&bob.ctx).await.unwrap(), true); // Step 7: Bob receives vc-contact-confirm bob.recv_msg_trash(&sent).await; From 222c03e5bacdeda4448331acd77f1b7af4cc2aac Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 12 Apr 2025 16:58:08 +0000 Subject: [PATCH 038/381] securejoin tests improvements --- src/contact.rs | 9 +++++---- src/securejoin/securejoin_tests.rs | 11 ++++------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/contact.rs b/src/contact.rs index 1f8e81f28f..b78efa984a 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -778,9 +778,10 @@ impl Contact { let id = context .sql .query_get_value( - "SELECT id FROM contacts \ - WHERE addr=?1 COLLATE NOCASE \ - AND id>?2 AND origin>=?3 AND (? OR blocked=?)", + "SELECT id FROM contacts + WHERE addr=?1 COLLATE NOCASE + AND fingerprint='' -- Do not lookup PGP-contacts + AND id>?2 AND origin>=?3 AND (? OR blocked=?)", ( &addr_normalized, ContactId::LAST_SPECIAL, @@ -1547,7 +1548,7 @@ impl Contact { .sql .query_get_value("SELECT verifier FROM contacts WHERE id=?", (self.id,)) .await? - .context("Contact does not exist")?; + .with_context(|| format!("Contact {} does not exist", self.id))?; if verifier_id == 0 { Ok(None) diff --git a/src/securejoin/securejoin_tests.rs b/src/securejoin/securejoin_tests.rs index 6f11e92027..702ac60001 100644 --- a/src/securejoin/securejoin_tests.rs +++ b/src/securejoin/securejoin_tests.rs @@ -544,7 +544,7 @@ async fn test_secure_join() -> Result<()> { "vg-auth-required" ); - // Step 4: Bob receives vg-auth-required, sends vg-request-with-auth + tcm.section("Step 4: Bob receives vg-auth-required, sends vg-request-with-auth"); bob.recv_msg_trash(&sent).await; let sent = bob.pop_sent_msg().await; @@ -585,13 +585,10 @@ async fn test_secure_join() -> Result<()> { ); // Alice should not yet have Bob verified - let contact_bob_id = Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown) - .await? - .expect("Contact not found"); - let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id).await?; + let contact_bob = alice.add_or_lookup_pgp_contact(&bob).await; assert_eq!(contact_bob.is_verified(&alice.ctx).await?, false); - // Step 5+6: Alice receives vg-request-with-auth, sends vg-member-added + tcm.section("Step 5+6: Alice receives vg-request-with-auth, sends vg-member-added"); alice.recv_msg_trash(&sent).await; assert_eq!(contact_bob.is_verified(&alice.ctx).await?, true); @@ -637,7 +634,7 @@ async fn test_secure_join() -> Result<()> { let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id).await?; assert_eq!(contact_alice.is_verified(&bob.ctx).await?, false); - // Step 7: Bob receives vg-member-added + tcm.section("Step 7: Bob receives vg-member-added"); bob.recv_msg(&sent).await; { // Bob has Alice verified, message shows up in the group chat. From 47f2d70dff5946a07ce5cf4d6a85e913fb45f8cf Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 12 Apr 2025 18:52:18 +0000 Subject: [PATCH 039/381] fixup the test --- src/decrypt.rs | 1 - src/peerstate.rs | 1 - src/securejoin/bob.rs | 8 ++++---- src/securejoin/securejoin_tests.rs | 28 ++++++++++------------------ 4 files changed, 14 insertions(+), 24 deletions(-) diff --git a/src/decrypt.rs b/src/decrypt.rs index d6bf2b8f5b..114683e441 100644 --- a/src/decrypt.rs +++ b/src/decrypt.rs @@ -9,7 +9,6 @@ use mailparse::ParsedMail; use crate::aheader::Aheader; use crate::context::Context; use crate::key::{Fingerprint, SignedPublicKey, SignedSecretKey}; -use crate::peerstate::Peerstate; use crate::pgp; /// Tries to decrypt a message, but only if it is structured as an Autocrypt message. diff --git a/src/peerstate.rs b/src/peerstate.rs index 6d145ae6c4..3d58f0dd93 100644 --- a/src/peerstate.rs +++ b/src/peerstate.rs @@ -7,7 +7,6 @@ use num_traits::FromPrimitive; use crate::aheader::{Aheader, EncryptPreference}; use crate::chat; use crate::chatlist::Chatlist; -use crate::config::Config; use crate::context::Context; use crate::key::{DcKey, Fingerprint, SignedPublicKey}; use crate::message::Message; diff --git a/src/securejoin/bob.rs b/src/securejoin/bob.rs index 4449a3dbac..0a0c5f3543 100644 --- a/src/securejoin/bob.rs +++ b/src/securejoin/bob.rs @@ -56,9 +56,6 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul // Now start the protocol and initialise the state. { - let peer_verified = - verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id()) - .await?; let has_key = context .sql .exists( @@ -67,7 +64,10 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul ) .await?; - if has_key && peer_verified { + if has_key + && verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id()) + .await? + { // The scanned fingerprint matches Alice's key, we can proceed to step 4b. info!(context, "Taking securejoin protocol shortcut"); send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::RequestWithAuth) diff --git a/src/securejoin/securejoin_tests.rs b/src/securejoin/securejoin_tests.rs index 702ac60001..161150ec01 100644 --- a/src/securejoin/securejoin_tests.rs +++ b/src/securejoin/securejoin_tests.rs @@ -95,7 +95,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) { 0 ); - // Step 1: Generate QR-code, ChatId(0) indicates setup-contact + tcm.section("Step 1: Generate QR-code, ChatId(0) indicates setup-contact"); let qr = get_securejoin_qr(&alice.ctx, None).await.unwrap(); // We want Bob to learn Alice's name from their messages, not from the QR code. alice @@ -103,16 +103,13 @@ async fn test_setup_contact_ex(case: SetupContactCase) { .await .unwrap(); - // Step 2: Bob scans QR-code, sends vc-request + tcm.section("Step 2: Bob scans QR-code, sends vc-request"); join_securejoin(&bob.ctx, &qr).await.unwrap(); assert_eq!( Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(), 1 ); - let contact_alice_id = Contact::lookup_id_by_addr(&bob.ctx, alice_addr, Origin::Unknown) - .await - .expect("Error looking up contact") - .expect("Contact not found"); + let contact_alice_id = bob.add_or_lookup_pgp_contact(&alice).await.id; let sent = bob.pop_sent_msg().await; assert!(!sent.payload.contains("Bob Examplenet")); assert_eq!(sent.recipient(), EmailAddress::new(alice_addr).unwrap()); @@ -122,7 +119,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) { assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some()); assert!(!msg.header_exists(HeaderDef::AutoSubmitted)); - // Step 3: Alice receives vc-request, sends vc-auth-required + tcm.section("Step 3: Alice receives vc-request, sends vc-auth-required"); alice.recv_msg_trash(&sent).await; assert_eq!( Chatlist::try_load(&alice, 0, None, None) @@ -152,7 +149,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) { assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true); } - // Step 4: Bob receives vc-auth-required, sends vc-request-with-auth + tcm.section("Step 4: Bob receives vc-auth-required, sends vc-request-with-auth"); bob.recv_msg_trash(&sent).await; let bob_chat = bob.get_pgp_chat(&alice).await; assert_eq!(bob_chat.why_cant_send(&bob).await.unwrap(), None); @@ -168,12 +165,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) { contact_id, progress, } => { - let alice_contact_id = - Contact::lookup_id_by_addr(&bob.ctx, alice_addr, Origin::Unknown) - .await - .expect("Error looking up contact") - .expect("Contact not found"); - assert_eq!(contact_id, alice_contact_id); + assert_eq!(contact_id, contact_alice_id); assert_eq!(progress, 400); } _ => unreachable!(), @@ -238,7 +230,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) { SystemTime::shift(Duration::from_secs(3600)); } - // Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm + tcm.section("Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm"); alice.recv_msg_trash(&sent).await; assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), true); let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id) @@ -504,12 +496,12 @@ async fn test_secure_join() -> Result<()> { let alice_chatid = chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat").await?; - // Step 1: Generate QR-code, secure-join implied by chatid + tcm.section("Step 1: Generate QR-code, secure-join implied by chatid"); let qr = get_securejoin_qr(&alice.ctx, Some(alice_chatid)) .await .unwrap(); - // Step 2: Bob scans QR-code, sends vg-request + tcm.section("Step 2: Bob scans QR-code, sends vg-request"); let bob_chatid = join_securejoin(&bob.ctx, &qr).await?; assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1); @@ -532,7 +524,7 @@ async fn test_secure_join() -> Result<()> { // is only sent in `vg-request-with-auth` for compatibility. assert!(!msg.header_exists(HeaderDef::SecureJoinGroup)); - // Step 3: Alice receives vg-request, sends vg-auth-required + tcm.section("Step 3: Alice receives vg-request, sends vg-auth-required"); alice.recv_msg_trash(&sent).await; let sent = alice.pop_sent_msg().await; From 1733c516a72e52a21ccb68b889b8dd4ad53a509b Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 12 Apr 2025 19:46:04 +0000 Subject: [PATCH 040/381] securejoin test fix --- src/securejoin/securejoin_tests.rs | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/securejoin/securejoin_tests.rs b/src/securejoin/securejoin_tests.rs index 161150ec01..ed72e145f7 100644 --- a/src/securejoin/securejoin_tests.rs +++ b/src/securejoin/securejoin_tests.rs @@ -540,6 +540,8 @@ async fn test_secure_join() -> Result<()> { bob.recv_msg_trash(&sent).await; let sent = bob.pop_sent_msg().await; + let contact_alice_id = bob.add_or_lookup_pgp_contact(&alice).await.id; + // Check Bob emitted the JoinerProgress event. let event = bob .evtracker @@ -550,12 +552,7 @@ async fn test_secure_join() -> Result<()> { contact_id, progress, } => { - let alice_contact_id = - Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown) - .await - .expect("Error looking up contact") - .expect("Contact not found"); - assert_eq!(contact_id, alice_contact_id); + assert_eq!(contact_id, contact_alice_id); assert_eq!(progress, 400); } _ => unreachable!(), @@ -617,14 +614,11 @@ async fn test_secure_join() -> Result<()> { assert_eq!(msg.get_text(), expected_text); } - // Bob should not yet have Alice verified - let contact_alice_id = - Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown) - .await - .expect("Error looking up contact") - .expect("Contact not found"); - let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id).await?; - assert_eq!(contact_alice.is_verified(&bob.ctx).await?, false); + // Bob has verified Alice already. + // + // Alice may not have verified Bob yet. + let contact_alice = bob.add_or_lookup_pgp_contact(&alice).await; + assert_eq!(contact_alice.is_verified(&bob.ctx).await?, true); tcm.section("Step 7: Bob receives vg-member-added"); bob.recv_msg(&sent).await; From f4ff41f70eaedca3dc911d0b7f332434aa99e4d0 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 12 Apr 2025 20:01:22 +0000 Subject: [PATCH 041/381] Add vCard for statistics bot --- assets/self-reporting-bot.vcf | 7 +++++++ src/context.rs | 36 ++++++++++------------------------- 2 files changed, 17 insertions(+), 26 deletions(-) create mode 100644 assets/self-reporting-bot.vcf diff --git a/assets/self-reporting-bot.vcf b/assets/self-reporting-bot.vcf new file mode 100644 index 0000000000..0e38035fe6 --- /dev/null +++ b/assets/self-reporting-bot.vcf @@ -0,0 +1,7 @@ +BEGIN:VCARD +VERSION:4.0 +EMAIL:self_reporting@testrun.org +FN:Statistics bot +KEY:data:application/pgp-keys;base64,xjMEZbfBlBYJKwYBBAHaRw8BAQdABpLWS2PUIGGo4pslVt4R8sylP5wZihmhf1DTDr3oCMPNHDxzZWxmX3JlcG9ydGluZ0B0ZXN0cnVuLm9yZz7CiwQQFggAMwIZAQUCZbfBlAIbAwQLCQgHBhUICQoLAgMWAgEWIQTS2i16sHeYTckGn284K3M5Z4oohAAKCRA4K3M5Z4oohD8dAQCQV7CoH6UP4PD+NqI4kW5tbbqdh2AnDROg60qotmLExAEAxDfd3QHAK9f8b9qQUbLmHIztCLxhEuVbWPBEYeVW0gvOOARlt8GUEgorBgEEAZdVAQUBAQdAMBUhYoAAcI625vGZqnM5maPX4sGJ7qvJxPAFILPy6AcDAQgHwngEGBYIACAFAmW3wZQCGwwWIQTS2i16sHeYTckGn284K3M5Z4oohAAKCRA4K3M5Z4oohPwCAQCvzk1ObIkj2GqsuIfaULlgdnfdZY8LNary425CEfHZDQD5AblXVrlMO1frdlc/Vo9z3pEeCrfYdD7ITD3/OeVoiQ4= +REV:20250412T195751Z +END:VCARD diff --git a/src/context.rs b/src/context.rs index 35d7c0a42c..28f6d784df 100644 --- a/src/context.rs +++ b/src/context.rs @@ -22,7 +22,7 @@ use crate::config::Config; use crate::constants::{ self, DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_CHAT_ID_TRASH, DC_VERSION_STR, }; -use crate::contact::{Contact, ContactId}; +use crate::contact::{Contact, ContactId, import_vcard}; use crate::debug_logging::DebugLogging; use crate::download::DownloadState; use crate::events::{Event, EventEmitter, EventType, Events}; @@ -32,7 +32,6 @@ use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam}; use crate::message::{self, Message, MessageState, MsgId}; use crate::param::{Param, Params}; use crate::peer_channels::Iroh; -use crate::peerstate::Peerstate; use crate::push::PushSubscriber; use crate::quota::QuotaInfo; use crate::scheduler::{convert_folder_meaning, SchedulerState}; @@ -1166,32 +1165,17 @@ impl Context { /// On the other end, a bot will receive the message and make it available /// to Delta Chat's developers. pub async fn draft_self_report(&self) -> Result { - const SELF_REPORTING_BOT: &str = "self_reporting@testrun.org"; + const SELF_REPORTING_BOT_VCARD: &str = include_str!("../assets/self-reporting-bot.vcf"); + let contact_id: ContactId = *import_vcard(self, SELF_REPORTING_BOT_VCARD) + .await? + .first() + .context("Self reporting bot vCard does not contain a contact")?; + self + .sql + .execute("UPDATE contacts SET verifier=?1 WHERE id=?1", (contact_id,)) + .await?; - let contact_id = Contact::create(self, "Statistics bot", SELF_REPORTING_BOT).await?; let chat_id = ChatId::create_for_contact(self, contact_id).await?; - - // We're including the bot's public key in Delta Chat - // so that the first message to the bot can directly be encrypted: - let public_key = SignedPublicKey::from_base64( - "xjMEZbfBlBYJKwYBBAHaRw8BAQdABpLWS2PUIGGo4pslVt4R8sylP5wZihmhf1DTDr3oCM\ - PNHDxzZWxmX3JlcG9ydGluZ0B0ZXN0cnVuLm9yZz7CiwQQFggAMwIZAQUCZbfBlAIbAwQLCQgHBhUI\ - CQoLAgMWAgEWIQTS2i16sHeYTckGn284K3M5Z4oohAAKCRA4K3M5Z4oohD8dAQCQV7CoH6UP4PD+Nq\ - I4kW5tbbqdh2AnDROg60qotmLExAEAxDfd3QHAK9f8b9qQUbLmHIztCLxhEuVbWPBEYeVW0gvOOARl\ - t8GUEgorBgEEAZdVAQUBAQdAMBUhYoAAcI625vGZqnM5maPX4sGJ7qvJxPAFILPy6AcDAQgHwngEGB\ - YIACAFAmW3wZQCGwwWIQTS2i16sHeYTckGn284K3M5Z4oohAAKCRA4K3M5Z4oohPwCAQCvzk1ObIkj\ - 2GqsuIfaULlgdnfdZY8LNary425CEfHZDQD5AblXVrlMO1frdlc/Vo9z3pEeCrfYdD7ITD3/OeVoiQ\ - 4=", - )?; - let mut peerstate = Peerstate::from_public_key( - SELF_REPORTING_BOT, - 0, - EncryptPreference::Mutual, - &public_key, - ); - let fingerprint = public_key.dc_fingerprint(); - peerstate.set_verified(public_key, fingerprint, "".to_string())?; - peerstate.save_to_db(&self.sql).await?; chat_id .set_protection(self, ProtectionStatus::Protected, time(), Some(contact_id)) .await?; From 5959d5ecfee83c33efe9ddefa48f21f44731b482 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 12 Apr 2025 20:24:29 +0000 Subject: [PATCH 042/381] cleanup --- src/contact.rs | 6 +++--- src/contact/contact_tests.rs | 2 +- src/context.rs | 7 ++----- src/decrypt.rs | 3 --- src/e2ee.rs | 1 - src/mimeparser.rs | 2 +- src/receive_imf/receive_imf_tests.rs | 1 + 7 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/contact.rs b/src/contact.rs index b78efa984a..bedca2ce43 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -1,6 +1,6 @@ //! Contacts module -use std::cmp::{min, Reverse}; +use std::cmp::Reverse; use std::collections::{BinaryHeap, HashSet}; use std::fmt; use std::path::{Path, PathBuf}; @@ -11,8 +11,8 @@ use async_channel::{self as channel, Receiver, Sender}; use base64::Engine as _; pub use deltachat_contact_tools::may_be_valid_addr; use deltachat_contact_tools::{ - self as contact_tools, addr_cmp, addr_normalize, sanitize_name, sanitize_name_and_addr, - ContactAddress, VcardContact, + self as contact_tools, addr_normalize, sanitize_name, sanitize_name_and_addr, ContactAddress, + VcardContact, }; use deltachat_derive::{FromSql, ToSql}; use rusqlite::OptionalExtension; diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs index ffc1c26a4c..3af128cf6e 100644 --- a/src/contact/contact_tests.rs +++ b/src/contact/contact_tests.rs @@ -1,4 +1,4 @@ -use deltachat_contact_tools::may_be_valid_addr; +use deltachat_contact_tools::{may_be_valid_addr, addr_cmp}; use super::*; use crate::chat::{get_chat_contacts, send_text_msg, Chat}; diff --git a/src/context.rs b/src/context.rs index 28f6d784df..ca3075132c 100644 --- a/src/context.rs +++ b/src/context.rs @@ -11,18 +11,16 @@ use std::time::Duration; use anyhow::{bail, ensure, Context as _, Result}; use async_channel::{self as channel, Receiver, Sender}; use pgp::types::PublicKeyTrait; -use pgp::SignedPublicKey; use ratelimit::Ratelimit; use tokio::sync::{Mutex, Notify, RwLock}; -use crate::aheader::EncryptPreference; use crate::chat::{get_chat_cnt, ChatId, ProtectionStatus}; use crate::chatlist_events; use crate::config::Config; use crate::constants::{ self, DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_CHAT_ID_TRASH, DC_VERSION_STR, }; -use crate::contact::{Contact, ContactId, import_vcard}; +use crate::contact::{import_vcard, Contact, ContactId}; use crate::debug_logging::DebugLogging; use crate::download::DownloadState; use crate::events::{Event, EventEmitter, EventType, Events}; @@ -1170,8 +1168,7 @@ impl Context { .await? .first() .context("Self reporting bot vCard does not contain a contact")?; - self - .sql + self.sql .execute("UPDATE contacts SET verifier=?1 WHERE id=?1", (contact_id,)) .await?; diff --git a/src/decrypt.rs b/src/decrypt.rs index 114683e441..b86d70fdb3 100644 --- a/src/decrypt.rs +++ b/src/decrypt.rs @@ -3,11 +3,8 @@ use std::collections::HashSet; use anyhow::Result; -use deltachat_contact_tools::addr_cmp; use mailparse::ParsedMail; -use crate::aheader::Aheader; -use crate::context::Context; use crate::key::{Fingerprint, SignedPublicKey, SignedSecretKey}; use crate::pgp; diff --git a/src/e2ee.rs b/src/e2ee.rs index 138f096bb0..8f6a0ba8a9 100644 --- a/src/e2ee.rs +++ b/src/e2ee.rs @@ -1,6 +1,5 @@ //! End-to-end encryption support. -use std::collections::BTreeSet; use std::io::Cursor; use anyhow::Result; diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 623fde4cbd..32e33fd1c6 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -21,7 +21,7 @@ use crate::config::Config; use crate::constants; use crate::contact::ContactId; use crate::context::Context; -use crate::decrypt::{get_encrypted_mime, try_decrypt, validate_detached_signature}; +use crate::decrypt::{try_decrypt, validate_detached_signature}; use crate::dehtml::dehtml; use crate::events::EventType; use crate::headerdef::{HeaderDef, HeaderDefMap}; diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 58f66b889b..340c7658b8 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -1,3 +1,4 @@ +use deltachat_contact_tools::addr_cmp; use rand::Rng; use std::time::Duration; From 084e67b4b5c580bec062c55c23f9dd7fef5be535 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 12 Apr 2025 23:32:22 +0000 Subject: [PATCH 043/381] less peerstate --- src/chat.rs | 7 +------ src/contact/contact_tests.rs | 2 +- src/mimefactory.rs | 1 - src/peerstate.rs | 11 ----------- src/receive_imf.rs | 2 -- 5 files changed, 2 insertions(+), 21 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index 7d0b7e2665..6e54ad5232 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -2673,12 +2673,7 @@ impl ChatIdBlocked { _ => (), } - let protected = contact_id == ContactId::SELF || { - let peerstate = Peerstate::from_addr(context, contact.get_addr()).await?; - peerstate.is_some_and(|p| { - p.is_using_verified_key() && p.prefer_encrypt == EncryptPreference::Mutual - }) - }; + let protected = contact_id == ContactId::SELF || contact.is_verified(context).await?; let smeared_time = create_smeared_timestamp(context); let chat_id = context diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs index 3af128cf6e..00e5e73a8b 100644 --- a/src/contact/contact_tests.rs +++ b/src/contact/contact_tests.rs @@ -1,4 +1,4 @@ -use deltachat_contact_tools::{may_be_valid_addr, addr_cmp}; +use deltachat_contact_tools::{addr_cmp, may_be_valid_addr}; use super::*; use crate::chat::{get_chat_contacts, send_text_msg, Chat}; diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 66e1b1d196..a25030c877 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -774,7 +774,6 @@ impl MimeFactory { )); } - let verified = self.verified(); let grpimage = self.grpimage(); let skip_autocrypt = self.should_skip_autocrypt(); let encrypt_helper = EncryptHelper::new(context).await?; diff --git a/src/peerstate.rs b/src/peerstate.rs index 3d58f0dd93..a455f63961 100644 --- a/src/peerstate.rs +++ b/src/peerstate.rs @@ -440,17 +440,6 @@ impl Peerstate { } } - /// Returns true if the key used for opportunistic encryption in the 1:1 chat - /// is the same as the verified key. - /// - /// Note that verified groups always use the verified key no matter if the - /// opportunistic key matches or not. - pub(crate) fn is_using_verified_key(&self) -> bool { - let verified = self.peek_key_fingerprint(true); - - verified.is_some() && verified == self.peek_key_fingerprint(false) - } - /// Set this peerstate to verified; /// make sure to call `self.save_to_db` to save these changes. /// diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 23ac18cad1..4b5b074818 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1029,8 +1029,6 @@ async fn add_parts( .await?; } restore_protection = new_protection != ProtectionStatus::Protected - // Check that the contact still has the Autocrypt key same as the - // verified key, see also `Peerstate::is_using_verified_key()`. && contact.is_verified(context).await?; } } From 02e4a3b9e62fdccc68be527424cd08879936ef1e Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 12 Apr 2025 23:33:14 +0000 Subject: [PATCH 044/381] cleanup --- src/chat.rs | 2 -- src/peerstate.rs | 23 ----------------------- src/receive_imf/receive_imf_tests.rs | 1 - 3 files changed, 26 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index 6e54ad5232..cada889209 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -17,7 +17,6 @@ use serde::{Deserialize, Serialize}; use strum_macros::EnumIter; use tokio::task; -use crate::aheader::EncryptPreference; use crate::blob::BlobObject; use crate::chatlist::Chatlist; use crate::color::str_to_color; @@ -39,7 +38,6 @@ use crate::message::{self, Message, MessageState, MsgId, Viewtype}; use crate::mimefactory::MimeFactory; use crate::mimeparser::SystemMessage; use crate::param::{Param, Params}; -use crate::peerstate::Peerstate; use crate::receive_imf::ReceivedMsg; use crate::smtp::send_msg_to_smtp; use crate::stock_str; diff --git a/src/peerstate.rs b/src/peerstate.rs index a455f63961..038483edb4 100644 --- a/src/peerstate.rs +++ b/src/peerstate.rs @@ -14,16 +14,6 @@ use crate::mimeparser::SystemMessage; use crate::sql::Sql; use crate::{chatlist_events, stock_str}; -/// Type of the public key stored inside the peerstate. -#[derive(Debug)] -pub enum PeerstateKeyType { - /// Public key sent in the `Autocrypt-Gossip` header. - GossipKey, - - /// Public key sent in the `Autocrypt` header. - PublicKey, -} - /// Peerstate represents the state of an Autocrypt peer. #[derive(Debug, PartialEq, Eq, Clone)] pub struct Peerstate { @@ -427,19 +417,6 @@ impl Peerstate { } } - /// Returns a reference to the contact's public key fingerprint. - /// - /// Similar to [`Self::peek_key`], but returns the fingerprint instead of the key. - fn peek_key_fingerprint(&self, verified: bool) -> Option<&Fingerprint> { - if verified { - self.verified_key_fingerprint.as_ref() - } else { - self.public_key_fingerprint - .as_ref() - .or(self.gossip_key_fingerprint.as_ref()) - } - } - /// Set this peerstate to verified; /// make sure to call `self.save_to_db` to save these changes. /// diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 340c7658b8..58f66b889b 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -1,4 +1,3 @@ -use deltachat_contact_tools::addr_cmp; use rand::Rng; use std::time::Duration; From 2b6112a7544a7d1e85daa5d14191222a80afcb48 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 12 Apr 2025 23:48:57 +0000 Subject: [PATCH 045/381] cleanup --- src/mimefactory.rs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index a25030c877..033be389d0 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -460,20 +460,6 @@ impl MimeFactory { Ok(res) } - fn verified(&self) -> bool { - match &self.loaded { - Loaded::Message { chat, msg } => { - chat.is_self_talk() || - // Securejoin messages are supposed to verify a key. - // In order to do this, it is necessary that they can be sent - // to a key that is not yet verified. - // This has to work independently of whether the chat is protected right now. - chat.is_protected() && msg.get_info_type() != SystemMessage::SecurejoinMessage - } - Loaded::Mdn { .. } => false, - } - } - fn should_skip_autocrypt(&self) -> bool { match &self.loaded { Loaded::Message { msg, .. } => { From 98ff233aa33a6710ace259de50f146a51dff8cc3 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 12 Apr 2025 23:56:32 +0000 Subject: [PATCH 046/381] mark contact as verified --- src/contact.rs | 35 ++++++++++++++++++++++++++++ src/context.rs | 6 ++--- src/receive_imf.rs | 7 ++---- src/receive_imf/receive_imf_tests.rs | 3 ++- src/securejoin.rs | 6 ++--- src/test_utils.rs | 31 +++++++----------------- 6 files changed, 51 insertions(+), 37 deletions(-) diff --git a/src/contact.rs b/src/contact.rs index bedca2ce43..bbfe65c5cc 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -1852,6 +1852,41 @@ pub(crate) async fn update_last_seen( Ok(()) } +/// Marks contact `contact_id` as verified by `verifier_id`. +pub(crate) async fn mark_contact_id_as_verified( + context: &Context, + contact_id: ContactId, + verifier_id: ContactId, +) -> Result<()> { + context + .sql + .transaction(|transaction| { + let contact_fingerprint: String = transaction.query_row( + "SELECT fingerprint FROM contacts WHERE id=?", + (contact_id,), + |row| row.get(0), + )?; + if contact_fingerprint.is_empty() { + bail!("Non-PGP contact {contact_id} cannot be verified."); + } + let verifier_fingerprint: String = transaction.query_row( + "SELECT fingerprint FROM contacts WHERE id=?", + (verifier_id,), + |row| row.get(0), + )?; + if verifier_fingerprint.is_empty() { + bail!("Contact {contact_id} cannot be verified by non-PGP contact {verifier_id}."); + } + transaction.execute( + "UPDATE contacts SET verifier=? WHERE id=?", + (verifier_id, contact_id), + )?; + Ok(()) + }) + .await?; + Ok(()) +} + fn cat_fingerprint(ret: &mut String, name: &str, addr: &str, fingerprint: &str) { *ret += &format!("\n\n{} ({}):\n{}", name, addr, fingerprint); } diff --git a/src/context.rs b/src/context.rs index ca3075132c..7347f52b46 100644 --- a/src/context.rs +++ b/src/context.rs @@ -20,7 +20,7 @@ use crate::config::Config; use crate::constants::{ self, DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_CHAT_ID_TRASH, DC_VERSION_STR, }; -use crate::contact::{import_vcard, Contact, ContactId}; +use crate::contact::{import_vcard, mark_contact_id_as_verified, Contact, ContactId}; use crate::debug_logging::DebugLogging; use crate::download::DownloadState; use crate::events::{Event, EventEmitter, EventType, Events}; @@ -1168,9 +1168,7 @@ impl Context { .await? .first() .context("Self reporting bot vCard does not contain a contact")?; - self.sql - .execute("UPDATE contacts SET verifier=?1 WHERE id=?1", (contact_id,)) - .await?; + mark_contact_id_as_verified(self, contact_id, contact_id).await?; let chat_id = ChatId::create_for_contact(self, contact_id).await?; chat_id diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 4b5b074818..22c2bf51a4 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -15,7 +15,7 @@ use regex::Regex; use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus}; use crate::config::Config; use crate::constants::{Blocked, Chattype, ShowEmails, DC_CHAT_ID_TRASH, EDITED_PREFIX}; -use crate::contact::{Contact, ContactId, Origin}; +use crate::contact::{mark_contact_id_as_verified, Contact, ContactId, Origin}; use crate::context::Context; use crate::debug_logging::maybe_set_logging_xdc_inner; use crate::download::DownloadState; @@ -3054,10 +3054,7 @@ async fn mark_recipients_as_verified( continue; } - context - .sql - .execute("UPDATE contacts SET verifier=? WHERE id=?", (from_id, id)) - .await?; + mark_contact_id_as_verified(context, id, from_id).await?; } Ok(()) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 58f66b889b..76d5819d0a 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -18,7 +18,8 @@ use crate::imap::prefetch_should_download; use crate::imex::{imex, ImexMode}; use crate::peerstate::Peerstate; use crate::securejoin::get_securejoin_qr; -use crate::test_utils::{get_chat_msg, mark_as_verified, TestContext, TestContextManager}; +use crate::test_utils::mark_as_verified; +use crate::test_utils::{get_chat_msg, TestContext, TestContextManager}; use crate::tools::{time, SystemTime}; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] diff --git a/src/securejoin.rs b/src/securejoin.rs index 39c97876de..98486a42d5 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -9,6 +9,7 @@ use crate::chat::{self, get_chat_id_by_grpid, Chat, ChatId, ChatIdBlocked, Prote use crate::chatlist_events; use crate::config::Config; use crate::constants::{Blocked, Chattype, NON_ALPHANUMERIC_WITHOUT_DOT}; +use crate::contact::mark_contact_id_as_verified; use crate::contact::{Contact, ContactId, Origin}; use crate::context::Context; use crate::e2ee::ensure_secret_key_exists; @@ -210,10 +211,7 @@ async fn verify_sender_by_fingerprint( let contact = Contact::get_by_id(context, contact_id).await?; let is_verified = contact.fingerprint().is_some_and(|fp| fp == fingerprint); if is_verified { - context - .sql - .execute("UPDATE contacts SET verifier=?1 WHERE id=?1", (contact_id,)) - .await?; + mark_contact_id_as_verified(context, contact_id, contact_id).await?; } Ok(is_verified) } diff --git a/src/test_utils.rs b/src/test_utils.rs index 1bb0c13183..1262120aa3 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -25,17 +25,17 @@ use crate::chat::{ }; use crate::chatlist::Chatlist; use crate::config::Config; -use crate::constants::DC_CHAT_ID_TRASH; -use crate::constants::DC_GCL_NO_SPECIALS; use crate::constants::{Blocked, Chattype}; -use crate::contact::{import_vcard, make_vcard, Contact, ContactId, Modifier, Origin}; +use crate::constants::{DC_CHAT_ID_TRASH, DC_GCL_NO_SPECIALS}; +use crate::contact::{ + import_vcard, make_vcard, mark_contact_id_as_verified, Contact, ContactId, Modifier, Origin, +}; use crate::context::Context; use crate::e2ee::EncryptHelper; use crate::events::{Event, EventEmitter, EventType, Events}; use crate::key::{self, load_self_public_key, DcKey, DcSecretKey}; use crate::message::{update_msg_state, Message, MessageState, MsgId, Viewtype}; use crate::mimeparser::{MimeMessage, SystemMessage}; -use crate::peerstate::Peerstate; use crate::pgp::KeyPair; use crate::receive_imf::receive_imf; use crate::securejoin::{get_securejoin_qr, join_securejoin}; @@ -1362,25 +1362,10 @@ fn print_logevent(logevent: &LogEvent) { /// Saves the other account's public key as verified /// and peerstate as backwards verified. pub(crate) async fn mark_as_verified(this: &TestContext, other: &TestContext) { - let mut peerstate = Peerstate::from_header( - &EncryptHelper::new(other).await.unwrap().get_aheader(), - // We have to give 0 as the time, not the current time: - // The time is going to be saved in peerstate.last_seen. - // The code in `peerstate.rs` then compares `if message_time > self.last_seen`, - // and many similar checks in peerstate.rs, and doesn't allow changes otherwise. - // Giving the current time would mean that message_time == peerstate.last_seen, - // so changes would not be allowed. - // This might lead to flaky tests. - 0, - ); - - peerstate.verified_key.clone_from(&peerstate.public_key); - peerstate - .verified_key_fingerprint - .clone_from(&peerstate.public_key_fingerprint); - peerstate.backward_verified_key_id = Some(this.get_config_i64(Config::KeyId).await.unwrap()); - - peerstate.save_to_db(&this.sql).await.unwrap(); + let contact_id = this.add_or_lookup_contact_id(other).await; + mark_contact_id_as_verified(this, contact_id, contact_id) + .await + .unwrap(); } /// Pops a sync message from alice0 and receives it on alice1. Should be used after an action on From 5f7f4293c2eab3e84e2afafe8f9927e7d0cfa730 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 13 Apr 2025 00:15:31 +0000 Subject: [PATCH 047/381] remove no autocrypt test --- src/e2ee.rs | 81 ----------------------------------------------- src/test_utils.rs | 1 - 2 files changed, 82 deletions(-) diff --git a/src/e2ee.rs b/src/e2ee.rs index 8f6a0ba8a9..f05f02eb3b 100644 --- a/src/e2ee.rs +++ b/src/e2ee.rs @@ -89,8 +89,6 @@ mod tests { use crate::chat::send_text_msg; use crate::config::Config; use crate::message::{Message, Viewtype}; - use crate::param::Param; - use crate::peerstate::Peerstate; use crate::receive_imf::receive_imf; use crate::test_utils::{TestContext, TestContextManager}; @@ -134,85 +132,6 @@ Sent with my Delta Chat Messenger: https://delta.chat"; ); } - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_encrypted_no_autocrypt() -> anyhow::Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - - let chat_alice = alice.create_email_chat(&bob).await.id; - let chat_bob = bob.create_email_chat(&alice).await.id; - - // Alice sends unencrypted message to Bob - let mut msg = Message::new(Viewtype::Text); - let sent = alice.send_msg(chat_alice, &mut msg).await; - - // Bob receives unencrypted message from Alice - let msg = bob.recv_msg(&sent).await; - assert!(!msg.get_showpadlock()); - - let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.org") - .await? - .expect("no peerstate found in the database"); - assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual); - - // Bob sends empty encrypted message to Alice - let mut msg = Message::new(Viewtype::Text); - let sent = bob.send_msg(chat_bob, &mut msg).await; - - // Alice receives an empty encrypted message from Bob. - // This is also a regression test for previously existing bug - // that resulted in no padlock on encrypted empty messages. - let msg = alice.recv_msg(&sent).await; - assert!(msg.get_showpadlock()); - - let peerstate_bob = Peerstate::from_addr(&alice.ctx, "bob@example.net") - .await? - .expect("no peerstate found in the database"); - assert_eq!(peerstate_bob.prefer_encrypt, EncryptPreference::Mutual); - - // Now Alice and Bob have established keys. - - // Alice sends encrypted message without Autocrypt header. - let mut msg = Message::new(Viewtype::Text); - msg.param.set_int(Param::SkipAutocrypt, 1); - let sent = alice.send_msg(chat_alice, &mut msg).await; - - let msg = bob.recv_msg(&sent).await; - assert!(msg.get_showpadlock()); - let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.org") - .await? - .expect("no peerstate found in the database"); - assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual); - - // Alice sends plaintext message with Autocrypt header. - let mut msg = Message::new(Viewtype::Text); - msg.force_plaintext(); - let sent = alice.send_msg(chat_alice, &mut msg).await; - - let msg = bob.recv_msg(&sent).await; - assert!(!msg.get_showpadlock()); - let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.org") - .await? - .expect("no peerstate found in the database"); - assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual); - - // Alice sends plaintext message without Autocrypt header. - let mut msg = Message::new(Viewtype::Text); - msg.force_plaintext(); - msg.param.set_int(Param::SkipAutocrypt, 1); - let sent = alice.send_msg(chat_alice, &mut msg).await; - - let msg = bob.recv_msg(&sent).await; - assert!(!msg.get_showpadlock()); - let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.org") - .await? - .expect("no peerstate found in the database"); - assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Reset); - - Ok(()) - } - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_chatmail_can_send_unencrypted() -> Result<()> { let mut tcm = TestContextManager::new(); diff --git a/src/test_utils.rs b/src/test_utils.rs index 1262120aa3..6e2e46c158 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -31,7 +31,6 @@ use crate::contact::{ import_vcard, make_vcard, mark_contact_id_as_verified, Contact, ContactId, Modifier, Origin, }; use crate::context::Context; -use crate::e2ee::EncryptHelper; use crate::events::{Event, EventEmitter, EventType, Events}; use crate::key::{self, load_self_public_key, DcKey, DcSecretKey}; use crate::message::{update_msg_state, Message, MessageState, MsgId, Viewtype}; From 9de9ca946335e08f48fc6b2554bf4457e21abdd7 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 13 Apr 2025 01:02:35 +0000 Subject: [PATCH 048/381] remove another peerstate test --- src/authres.rs | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/src/authres.rs b/src/authres.rs index 21879b0f4b..0ac9284343 100644 --- a/src/authres.rs +++ b/src/authres.rs @@ -266,7 +266,6 @@ mod tests { use super::*; use crate::mimeparser; - use crate::peerstate::Peerstate; use crate::test_utils::TestContext; use crate::test_utils::TestContextManager; use crate::tools; @@ -520,41 +519,6 @@ Authentication-Results: dkim="; handle_authres(&t, &mail, "invalid@rom.com").await.unwrap(); } - // Test that Autocrypt works with mailing list. - // - // Previous versions of Delta Chat ignored Autocrypt based on the List-Post header. - // This is not needed: comparing of the From address to Autocrypt header address is enough. - // If the mailing list is not rewriting the From header, Autocrypt should be applied. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_autocrypt_in_mailinglist_not_ignored() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - - let alice_bob_chat = alice.create_chat(&bob).await; - let bob_alice_chat = bob.create_chat(&alice).await; - let mut sent = alice.send_text(alice_bob_chat.id, "hellooo").await; - sent.payload - .insert_str(0, "List-Post: \n"); - bob.recv_msg(&sent).await; - let peerstate = Peerstate::from_addr(&bob, "alice@example.org").await?; - assert!(peerstate.is_some()); - - // Bob can now write encrypted to Alice: - let mut sent = bob - .send_text(bob_alice_chat.id, "hellooo in the mailinglist again") - .await; - assert!(sent.load_from_db().await.get_showpadlock()); - - sent.payload - .insert_str(0, "List-Post: \n"); - let rcvd = alice.recv_msg(&sent).await; - assert!(rcvd.get_showpadlock()); - assert_eq!(&rcvd.text, "hellooo in the mailinglist again"); - - Ok(()) - } - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_authres_in_mailinglist_ignored() -> Result<()> { let mut tcm = TestContextManager::new(); From 8fa81c7b1b6b44529ff1c38c1d10f4ac2e249af6 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 13 Apr 2025 01:14:00 +0000 Subject: [PATCH 049/381] less peerstate --- src/qr.rs | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/src/qr.rs b/src/qr.rs index f3c101054a..f0c3913991 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -21,7 +21,6 @@ use crate::key::Fingerprint; use crate::message::Message; use crate::net::http::post_empty; use crate::net::proxy::{ProxyConfig, DEFAULT_SOCKS_PORT}; -use crate::peerstate::Peerstate; use crate::token; use crate::tools::validate_id; @@ -529,28 +528,14 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result { }) } } else if let Some(addr) = addr { - // FIXME don't use peerstate + let fingerprint = fingerprint.hex(); + let (contact_id, _) = Contact::add_or_lookup_ex(context, "", &addr, &fingerprint, Origin::UnhandledQrScan).await?; + let contact = Contact::get_by_id(context, contact_id).await?; - // retrieve known state for this fingerprint - let peerstate = Peerstate::from_fingerprint(context, &fingerprint) - .await - .context("Can't load peerstate")?; - - if let Some(peerstate) = peerstate { - let peerstate_addr = ContactAddress::new(&peerstate.addr)?; - let (contact_id, _) = - Contact::add_or_lookup(context, &name, &peerstate_addr, Origin::UnhandledQrScan) - .await - .context("add_or_lookup")?; - ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Request) - .await - .context("Failed to create (new) chat for contact")?; + if contact.openpgp_certificate(context).await?.is_some() { Ok(Qr::FprOk { contact_id }) } else { - let contact_id = Contact::lookup_id_by_addr(context, &addr, Origin::Unknown) - .await - .with_context(|| format!("Error looking up contact {addr:?}"))?; - Ok(Qr::FprMismatch { contact_id }) + Ok(Qr::FprMismatch { contact_id: Some(contact_id) }) } } else { Ok(Qr::FprWithoutAddr { From 8cf0da111a5720a4aa382c4c80d00c68a0e884ea Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 13 Apr 2025 01:19:38 +0000 Subject: [PATCH 050/381] even less peerstate --- src/mimeparser.rs | 22 ++++++------- src/peerstate.rs | 82 ---------------------------------------------- src/qr/qr_tests.rs | 1 + 3 files changed, 11 insertions(+), 94 deletions(-) diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 32e33fd1c6..0d88ba12fd 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -1940,18 +1940,16 @@ async fn update_gossip_peerstates( continue; } - let peerstate; - if let Some(mut p) = Peerstate::from_addr(context, &header.addr).await? { - p.apply_gossip(&header, message_time); - p.save_to_db(&context.sql).await?; - peerstate = p; - } else { - let p = Peerstate::from_gossip(&header, message_time); - p.save_to_db(&context.sql).await?; - peerstate = p; - }; - peerstate - .handle_fingerprint_change(context, message_time) + let fingerprint = header.public_key.dc_fingerprint().hex(); + context + .sql + .execute( + "INSERT INTO public_keys (fingerprint, public_key) + VALUES (?, ?) + ON CONFLICT (fingerprint) + DO NOTHING", + (&fingerprint, header.public_key.to_bytes()), + ) .await?; gossiped_keys.insert(header.addr.to_lowercase(), header.public_key); diff --git a/src/peerstate.rs b/src/peerstate.rs index 038483edb4..8a921f8956 100644 --- a/src/peerstate.rs +++ b/src/peerstate.rs @@ -556,88 +556,6 @@ impl Peerstate { pub fn get_verifier(&self) -> Option<&str> { self.verifier.as_deref() } - - /// Add an info message to all the chats with this contact, informing about - /// a [`PeerstateChange`]. - /// - /// Also, in the case of an address change (AEAP), replace the old address - /// with the new address in all chats. - async fn handle_setup_change( - &self, - context: &Context, - timestamp: i64, - change: PeerstateChange, - ) -> Result<()> { - if context.is_self_addr(&self.addr).await? { - // Do not try to search all the chats with self. - return Ok(()); - } - - let contact_id = context - .sql - .query_get_value( - "SELECT id FROM contacts WHERE addr=? COLLATE NOCASE;", - (&self.addr,), - ) - .await? - .with_context(|| format!("contact with peerstate.addr {:?} not found", &self.addr))?; - - let chats = Chatlist::try_load(context, 0, None, Some(contact_id)).await?; - let msg = match &change { - PeerstateChange::FingerprintChange => { - stock_str::contact_setup_changed(context, &self.addr).await - } - }; - for (chat_id, msg_id) in chats.iter() { - let timestamp_sort = if let Some(msg_id) = msg_id { - let lastmsg = Message::load_from_db(context, *msg_id).await?; - lastmsg.timestamp_sort - } else { - chat_id.created_timestamp(context).await? - }; - - chat::add_info_msg_with_cmd( - context, - *chat_id, - &msg, - SystemMessage::Unknown, - timestamp_sort, - Some(timestamp), - None, - None, - None, - ) - .await?; - } - - chatlist_events::emit_chatlist_changed(context); - // update the chats the contact is part of - chatlist_events::emit_chatlist_items_changed_for_contact(context, contact_id); - Ok(()) - } - - /// Adds a warning to all the chats corresponding to peerstate if fingerprint has changed. - pub(crate) async fn handle_fingerprint_change( - &self, - context: &Context, - timestamp: i64, - ) -> Result<()> { - if self.fingerprint_changed { - self.handle_setup_change(context, timestamp, PeerstateChange::FingerprintChange) - .await?; - } - Ok(()) - } -} - -/// Type of the peerstate change. -/// -/// Changes to the peerstate are notified to the user via a message -/// explaining the happened change. -enum PeerstateChange { - /// The contact's public key fingerprint changed, likely because - /// the contact uses a new device and didn't transfer their key. - FingerprintChange, } #[cfg(test)] diff --git a/src/qr/qr_tests.rs b/src/qr/qr_tests.rs index 77713e0c81..892452ebfe 100644 --- a/src/qr/qr_tests.rs +++ b/src/qr/qr_tests.rs @@ -5,6 +5,7 @@ use crate::config::Config; use crate::key::DcKey; use crate::securejoin::get_securejoin_qr; use crate::test_utils::{alice_keypair, TestContext}; +use crate::peerstate::Peerstate; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_decode_http() -> Result<()> { From c66875ba93a7adde06fbffc36ad1aae56d846bdc Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 13 Apr 2025 01:26:21 +0000 Subject: [PATCH 051/381] observe securejoin --- src/securejoin.rs | 57 +++-------------------------------------------- 1 file changed, 3 insertions(+), 54 deletions(-) diff --git a/src/securejoin.rs b/src/securejoin.rs index 98486a42d5..40d48fcb0a 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -19,7 +19,6 @@ use crate::key::{load_self_public_key, DcKey, Fingerprint}; use crate::message::{Message, Viewtype}; use crate::mimeparser::{MimeMessage, SystemMessage}; use crate::param::Param; -use crate::peerstate::Peerstate; use crate::qr::check_qr; use crate::securejoin::bob::JoinerProgress; use crate::stock_str; @@ -496,17 +495,15 @@ pub(crate) async fn handle_securejoin_handshake( /// /// If we see self-sent {vc,vg}-request-with-auth, /// we know that we are Bob (joiner-observer) -/// that just marked peer (Alice) as forward-verified +/// that just marked peer (Alice) as verified /// either after receiving {vc,vg}-auth-required /// or immediately after scanning the QR-code /// if the key was already known. /// /// If we see self-sent vc-contact-confirm or vg-member-added message, /// we know that we are Alice (inviter-observer) -/// that just marked peer (Bob) as forward (and backward)-verified +/// that just marked peer (Bob) as verified /// in response to correct vc-request-with-auth message. -/// -/// In both cases we can mark the peer as forward-verified. pub(crate) async fn observe_securejoin_on_other_device( context: &Context, mime_message: &MimeMessage, @@ -528,13 +525,6 @@ pub(crate) async fn observe_securejoin_on_other_device( }; if !encrypted_and_signed(context, mime_message, &get_self_fingerprint(context).await?) { - could_not_establish_secure_connection( - context, - contact_id, - info_chat_id(context, contact_id).await?, - "Message not encrypted correctly.", - ) - .await?; return Ok(HandshakeMessage::Ignore); } @@ -544,51 +534,10 @@ pub(crate) async fn observe_securejoin_on_other_device( .to_lowercase(); let Some(key) = mime_message.gossiped_keys.get(&addr) else { - could_not_establish_secure_connection( - context, - contact_id, - info_chat_id(context, contact_id).await?, - &format!( - "No gossip header for '{}' at step {}, please update Delta Chat on all \ - your devices.", - &addr, step, - ), - ) - .await?; return Ok(HandshakeMessage::Ignore); }; - let Some(mut peerstate) = Peerstate::from_addr(context, &addr).await? else { - could_not_establish_secure_connection( - context, - contact_id, - info_chat_id(context, contact_id).await?, - &format!("No peerstate in db for '{}' at step {}", &addr, step), - ) - .await?; - return Ok(HandshakeMessage::Ignore); - }; - - let Some(fingerprint) = peerstate.gossip_key_fingerprint.clone() else { - could_not_establish_secure_connection( - context, - contact_id, - info_chat_id(context, contact_id).await?, - &format!( - "No gossip key fingerprint in db for '{}' at step {}", - &addr, step, - ), - ) - .await?; - return Ok(HandshakeMessage::Ignore); - }; - peerstate.set_verified(key.clone(), fingerprint, addr)?; - if matches!(step, "vg-member-added" | "vc-contact-confirm") { - peerstate.backward_verified_key_id = - Some(context.get_config_i64(Config::KeyId).await?).filter(|&id| id > 0); - } - peerstate.prefer_encrypt = EncryptPreference::Mutual; - peerstate.save_to_db(&context.sql).await?; + mark_contact_id_as_verified(context, contact_id, contact_id).await?; ChatId::set_protection_for_contact(context, contact_id, mime_message.timestamp_sent).await?; From 130223e8c0a405da4ec5b5f46037db90feccb0d1 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 13 Apr 2025 04:46:26 +0000 Subject: [PATCH 052/381] qr tests without peerstate --- src/e2ee.rs | 2 +- src/qr.rs | 8 +++-- src/qr/qr_tests.rs | 54 ++++++++---------------------- src/securejoin.rs | 2 ++ src/securejoin/securejoin_tests.rs | 6 ++-- 5 files changed, 27 insertions(+), 45 deletions(-) diff --git a/src/e2ee.rs b/src/e2ee.rs index f05f02eb3b..3c967cc27a 100644 --- a/src/e2ee.rs +++ b/src/e2ee.rs @@ -88,7 +88,7 @@ mod tests { use super::*; use crate::chat::send_text_msg; use crate::config::Config; - use crate::message::{Message, Viewtype}; + use crate::message::Message; use crate::receive_imf::receive_imf; use crate::test_utils::{TestContext, TestContextManager}; diff --git a/src/qr.rs b/src/qr.rs index f0c3913991..e2b9648332 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -529,13 +529,17 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result { } } else if let Some(addr) = addr { let fingerprint = fingerprint.hex(); - let (contact_id, _) = Contact::add_or_lookup_ex(context, "", &addr, &fingerprint, Origin::UnhandledQrScan).await?; + let (contact_id, _) = + Contact::add_or_lookup_ex(context, "", &addr, &fingerprint, Origin::UnhandledQrScan) + .await?; let contact = Contact::get_by_id(context, contact_id).await?; if contact.openpgp_certificate(context).await?.is_some() { Ok(Qr::FprOk { contact_id }) } else { - Ok(Qr::FprMismatch { contact_id: Some(contact_id) }) + Ok(Qr::FprMismatch { + contact_id: Some(contact_id), + }) } } else { Ok(Qr::FprWithoutAddr { diff --git a/src/qr/qr_tests.rs b/src/qr/qr_tests.rs index 892452ebfe..7b7aae0fe7 100644 --- a/src/qr/qr_tests.rs +++ b/src/qr/qr_tests.rs @@ -1,11 +1,8 @@ use super::*; -use crate::aheader::EncryptPreference; use crate::chat::{create_group_chat, ProtectionStatus}; use crate::config::Config; -use crate::key::DcKey; use crate::securejoin::get_securejoin_qr; -use crate::test_utils::{alice_keypair, TestContext}; -use crate::peerstate::Peerstate; +use crate::test_utils::{TestContext, TestContextManager}; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_decode_http() -> Result<()> { @@ -354,52 +351,29 @@ async fn test_decode_openpgp_secure_join() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_decode_openpgp_fingerprint() -> Result<()> { - let ctx = TestContext::new().await; + let mut tcm = TestContextManager::new(); + let bob = &tcm.bob().await; + let alice = &tcm.alice().await; - let alice_contact_id = Contact::create(&ctx, "Alice", "alice@example.org") - .await - .context("failed to create contact")?; - let pub_key = alice_keypair().public; - let peerstate = Peerstate { - addr: "alice@example.org".to_string(), - last_seen: 1, - last_seen_autocrypt: 1, - prefer_encrypt: EncryptPreference::Mutual, - public_key: Some(pub_key.clone()), - public_key_fingerprint: Some(pub_key.dc_fingerprint()), - gossip_key: None, - gossip_timestamp: 0, - gossip_key_fingerprint: None, - verified_key: None, - verified_key_fingerprint: None, - verifier: None, - secondary_verified_key: None, - secondary_verified_key_fingerprint: None, - secondary_verifier: None, - backward_verified_key_id: None, - fingerprint_changed: false, - }; - assert!( - peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(), - "failed to save peerstate" - ); + let alice_contact = bob.add_or_lookup_contact(alice).await; + let alice_contact_id = alice_contact.id; let qr = check_qr( - &ctx.ctx, + bob, "OPENPGP4FPR:1234567890123456789012345678901234567890#a=alice@example.org", ) .await?; if let Qr::FprMismatch { contact_id, .. } = qr { - assert_eq!(contact_id, Some(alice_contact_id)); + assert_ne!(contact_id.unwrap(), alice_contact_id); } else { bail!("Wrong QR code type"); } let qr = check_qr( - &ctx.ctx, + bob, &format!( "OPENPGP4FPR:{}#a=alice@example.org", - pub_key.dc_fingerprint() + alice_contact.fingerprint().unwrap() ), ) .await?; @@ -409,14 +383,14 @@ async fn test_decode_openpgp_fingerprint() -> Result<()> { bail!("Wrong QR code type"); } - assert_eq!( + assert!(matches!( check_qr( - &ctx.ctx, + bob, "OPENPGP4FPR:1234567890123456789012345678901234567890#a=bob@example.org", ) .await?, - Qr::FprMismatch { contact_id: None } - ); + Qr::FprMismatch { .. } + )); Ok(()) } diff --git a/src/securejoin.rs b/src/securejoin.rs index 40d48fcb0a..b9f9cf6605 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -534,6 +534,8 @@ pub(crate) async fn observe_securejoin_on_other_device( .to_lowercase(); let Some(key) = mime_message.gossiped_keys.get(&addr) else { + // TODO: check that contact_id fingerprint is the same as gossiped key fingerprint + return Ok(HandshakeMessage::Ignore); }; diff --git a/src/securejoin/securejoin_tests.rs b/src/securejoin/securejoin_tests.rs index ed72e145f7..04f2886464 100644 --- a/src/securejoin/securejoin_tests.rs +++ b/src/securejoin/securejoin_tests.rs @@ -5,10 +5,12 @@ use crate::chat::{remove_contact_from_chat, CantSendReason}; use crate::chatlist::Chatlist; use crate::constants::{self, Chattype}; use crate::imex::{imex, ImexMode}; +use crate::peerstate::Peerstate; use crate::receive_imf::receive_imf; use crate::stock_str::{self, chat_protection_enabled}; -use crate::test_utils::{get_chat_msg, TimeShiftFalsePositiveNote}; -use crate::test_utils::{TestContext, TestContextManager}; +use crate::test_utils::{ + get_chat_msg, TestContext, TestContextManager, TimeShiftFalsePositiveNote, +}; use crate::tools::SystemTime; use std::collections::HashSet; use std::time::Duration; From 8d626837f47cf88708170f9ef74117bf21e0380d Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 13 Apr 2025 05:20:58 +0000 Subject: [PATCH 053/381] no peerstates in thunderbird tests --- src/receive_imf/receive_imf_tests.rs | 48 ++++++---------------------- 1 file changed, 9 insertions(+), 39 deletions(-) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 76d5819d0a..e8ebfd040b 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -16,7 +16,6 @@ use crate::contact; use crate::download::MIN_DOWNLOAD_LIMIT; use crate::imap::prefetch_should_download; use crate::imex::{imex, ImexMode}; -use crate::peerstate::Peerstate; use crate::securejoin::get_securejoin_qr; use crate::test_utils::mark_as_verified; use crate::test_utils::{get_chat_msg, TestContext, TestContextManager}; @@ -3640,10 +3639,8 @@ async fn test_thunderbird_autocrypt() -> Result<()> { let raw = include_bytes!("../../test-data/message/thunderbird_with_autocrypt.eml"); let received_msg = receive_imf(&t, raw, false).await?.unwrap(); - let peerstate = Peerstate::from_addr(&t, "alice@example.org") - .await? - .unwrap(); - assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual); + // TODO: if the message should arrive as PGP-contact + // with the key taken from Autocrypt header. Ok(()) } @@ -3655,35 +3652,11 @@ async fn test_prefer_encrypt_mutual_if_encrypted() -> Result<()> { let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_signed_with_pubkey.eml"); receive_imf(&t, raw, false).await?; - let peerstate = Peerstate::from_addr(&t, "alice@example.org") - .await? - .unwrap(); - assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual); - - receive_imf( - &t, - b"From: alice@example.org\n\ - To: bob@example.net\n\ - Subject: foo\n\ - Message-ID: \n\ - Date: Thu, 2 Nov 2023 02:20:28 -0300\n\ - \n\ - unencrypted\n", - false, - ) - .await?; - let peerstate = Peerstate::from_addr(&t, "alice@example.org") - .await? - .unwrap(); - assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Reset); + // TODO: the message should arrive as PGP-contact with a key. let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_signed.eml"); receive_imf(&t, raw, false).await?; - let peerstate = Peerstate::from_addr(&t, "alice@example.org") - .await? - .unwrap(); - assert!(peerstate.public_key.is_some()); - assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual); + // TODO: the message should arrive as the same PGP-contact. Ok(()) } @@ -3725,17 +3698,14 @@ async fn test_thunderbird_autocrypt_unencrypted() -> Result<()> { let raw = include_bytes!("../../test-data/message/thunderbird_with_autocrypt_unencrypted.eml"); receive_imf(&t, raw, false).await?; - let peerstate = Peerstate::from_addr(&t, "alice@example.org") - .await? - .unwrap(); - assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual); + + // TODO: the message should arrive as email-contact let raw = include_bytes!("../../test-data/message/thunderbird_signed_unencrypted.eml"); receive_imf(&t, raw, false).await?; - let peerstate = Peerstate::from_addr(&t, "alice@example.org") - .await? - .unwrap(); - assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual); + + // TODO: the message should arrive as email-contact? + // or PGP-contact, but no padlock Ok(()) } From 116ec3da23e24667935c41e6991a3e53768918dc Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 13 Apr 2025 05:30:51 +0000 Subject: [PATCH 054/381] no mimeparser peerstate --- src/mimeparser.rs | 59 +++++++++++++++-------------------------------- 1 file changed, 19 insertions(+), 40 deletions(-) diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 0d88ba12fd..dd316f56b8 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -28,7 +28,6 @@ use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::key::{self, load_self_secret_keyring, DcKey, Fingerprint, SignedPublicKey}; use crate::message::{self, get_vcard_summary, set_msg_failed, Message, MsgId, Viewtype}; use crate::param::{Param, Params}; -use crate::peerstate::Peerstate; use crate::simplify::{simplify, SimplifiedText}; use crate::sync::SyncItems; use crate::tools::time; @@ -1334,14 +1333,12 @@ impl MimeMessage { } // Process attached PGP keys. - /* if mime_type.type_() == mime::APPLICATION && mime_type.subtype().as_str() == "pgp-keys" - && Self::try_set_peer_key_from_file_part(context, peerstate, decoded_data).await? + && Self::try_set_peer_key_from_file_part(context, decoded_data).await? { return Ok(()); } - */ let mut part = Part::default(); let msg_type = if context .is_webxdc_file(filename, decoded_data) @@ -1433,10 +1430,9 @@ impl MimeMessage { Ok(()) } - /// Returns whether a key from the attachment was set as peer's pubkey. + /// Returns whether a key from the attachment was saved. async fn try_set_peer_key_from_file_part( context: &Context, - peerstate: &mut Peerstate, decoded_data: &[u8], ) -> Result { let key = match str::from_utf8(decoded_data) { @@ -1448,47 +1444,29 @@ impl MimeMessage { }; let key = match SignedPublicKey::from_asc(key) { Err(err) => { - warn!( - context, - "PGP key attachment is not an ASCII-armored file: {:#}", err - ); + warn!(context, "PGP key attachment is not an ASCII-armored file: {err:#}."); return Ok(false); } Ok((key, _)) => key, }; if let Err(err) = key.verify() { - warn!(context, "attached PGP key verification failed: {}", err); - return Ok(false); - } - if !key.details.users.iter().any(|user| { - user.id - .id() - .ends_with((String::from("<") + &peerstate.addr + ">").as_bytes()) - }) { + warn!(context, "Attached PGP key verification failed: {err:#}."); return Ok(false); } - if let Some(curr_key) = &peerstate.public_key { - if key != *curr_key && peerstate.prefer_encrypt != EncryptPreference::Reset { - // We don't want to break the existing Autocrypt setup. Yes, it's unlikely that a - // user have an Autocrypt-capable MUA and also attaches a key, but if that's the - // case, let 'em first disable Autocrypt and then change the key by attaching it. - warn!( - context, - "not using attached PGP key for peer '{}' because another one is already set \ - with prefer-encrypt={}", - peerstate.addr, - peerstate.prefer_encrypt, - ); - return Ok(false); - } - } - peerstate.public_key = Some(key); - info!( - context, - "using attached PGP key for peer '{}' with prefer-encrypt=mutual", peerstate.addr, - ); - peerstate.prefer_encrypt = EncryptPreference::Mutual; - peerstate.save_to_db(&context.sql).await?; + + let fingerprint = key.dc_fingerprint().hex(); + context + .sql + .execute( + "INSERT INTO public_keys (fingerprint, public_key) + VALUES (?, ?) + ON CONFLICT (fingerprint) + DO NOTHING", + (&fingerprint, key.to_bytes()), + ) + .await?; + + info!(context, "Imported PGP key {fingerprint} from attachment."); Ok(true) } @@ -1941,6 +1919,7 @@ async fn update_gossip_peerstates( } let fingerprint = header.public_key.dc_fingerprint().hex(); + // TODO: header.public_key.verify() ? context .sql .execute( From 71f44182a7f7a6420ab3d4e24c4a671b7e5e90f8 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 13 Apr 2025 05:32:50 +0000 Subject: [PATCH 055/381] no peerstate --- src/lib.rs | 1 - src/peerstate.rs | 758 ----------------------------- src/securejoin/securejoin_tests.rs | 28 +- src/tests/aeap.rs | 10 - 4 files changed, 1 insertion(+), 796 deletions(-) delete mode 100644 src/peerstate.rs diff --git a/src/lib.rs b/src/lib.rs index 9e9cc49cca..3c6402cbfe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,7 +74,6 @@ mod mimefactory; pub mod mimeparser; pub mod oauth2; mod param; -pub mod peerstate; mod pgp; pub mod provider; pub mod qr; diff --git a/src/peerstate.rs b/src/peerstate.rs deleted file mode 100644 index 8a921f8956..0000000000 --- a/src/peerstate.rs +++ /dev/null @@ -1,758 +0,0 @@ -//! # [Autocrypt Peer State](https://autocrypt.org/level1.html#peer-state-management) module. - -use anyhow::{Context as _, Error, Result}; -use deltachat_contact_tools::addr_cmp; -use num_traits::FromPrimitive; - -use crate::aheader::{Aheader, EncryptPreference}; -use crate::chat; -use crate::chatlist::Chatlist; -use crate::context::Context; -use crate::key::{DcKey, Fingerprint, SignedPublicKey}; -use crate::message::Message; -use crate::mimeparser::SystemMessage; -use crate::sql::Sql; -use crate::{chatlist_events, stock_str}; - -/// Peerstate represents the state of an Autocrypt peer. -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct Peerstate { - /// E-mail address of the contact. - pub addr: String, - - /// Timestamp of the latest peerstate update. - /// - /// Updated when a message is received from a contact, - /// either with or without `Autocrypt` header. - pub last_seen: i64, - - /// Timestamp of the latest `Autocrypt` header reception. - pub last_seen_autocrypt: i64, - - /// Encryption preference of the contact. - pub prefer_encrypt: EncryptPreference, - - /// Public key of the contact received in `Autocrypt` header. - pub public_key: Option, - - /// Fingerprint of the contact public key. - pub public_key_fingerprint: Option, - - /// Public key of the contact received in `Autocrypt-Gossip` header. - pub gossip_key: Option, - - /// Timestamp of the latest `Autocrypt-Gossip` header reception. - /// - /// It is stored to avoid applying outdated gossiped key - /// from delayed or reordered messages. - pub gossip_timestamp: i64, - - /// Fingerprint of the contact gossip key. - pub gossip_key_fingerprint: Option, - - /// Public key of the contact at the time it was verified, - /// either directly or via gossip from the verified contact. - pub verified_key: Option, - - /// Fingerprint of the verified public key. - pub verified_key_fingerprint: Option, - - /// The address that introduced this verified key. - pub verifier: Option, - - /// Secondary public verified key of the contact. - /// It could be a contact gossiped by another verified contact in a shared group - /// or a key that was previously used as a verified key. - pub secondary_verified_key: Option, - - /// Fingerprint of the secondary verified public key. - pub secondary_verified_key_fingerprint: Option, - - /// The address that introduced secondary verified key. - pub secondary_verifier: Option, - - /// Row ID of the key in the `keypairs` table - /// that we think the peer knows as verified. - pub backward_verified_key_id: Option, - - /// True if it was detected - /// that the fingerprint of the key used in chats with - /// opportunistic encryption was changed after Peerstate creation. - pub fingerprint_changed: bool, -} - -impl Peerstate { - /// Creates a peerstate from the `Autocrypt` header. - pub fn from_header(header: &Aheader, message_time: i64) -> Self { - Self::from_public_key( - &header.addr, - message_time, - header.prefer_encrypt, - &header.public_key, - ) - } - - /// Creates a peerstate from the given public key. - pub fn from_public_key( - addr: &str, - last_seen: i64, - prefer_encrypt: EncryptPreference, - public_key: &SignedPublicKey, - ) -> Self { - Peerstate { - addr: addr.to_string(), - last_seen, - last_seen_autocrypt: last_seen, - prefer_encrypt, - public_key: Some(public_key.clone()), - public_key_fingerprint: Some(public_key.dc_fingerprint()), - gossip_key: None, - gossip_key_fingerprint: None, - gossip_timestamp: 0, - verified_key: None, - verified_key_fingerprint: None, - verifier: None, - secondary_verified_key: None, - secondary_verified_key_fingerprint: None, - secondary_verifier: None, - backward_verified_key_id: None, - fingerprint_changed: false, - } - } - - /// Create a peerstate from the `Autocrypt-Gossip` header. - pub fn from_gossip(gossip_header: &Aheader, message_time: i64) -> Self { - Peerstate { - addr: gossip_header.addr.clone(), - last_seen: 0, - last_seen_autocrypt: 0, - - // Non-standard extension. According to Autocrypt 1.1.0 gossip headers SHOULD NOT - // contain encryption preference. - // - // Delta Chat includes encryption preference to ensure new users introduced to a group - // learn encryption preferences of other members immediately and don't send unencrypted - // messages to a group where everyone prefers encryption. - prefer_encrypt: gossip_header.prefer_encrypt, - public_key: None, - public_key_fingerprint: None, - gossip_key: Some(gossip_header.public_key.clone()), - gossip_key_fingerprint: Some(gossip_header.public_key.dc_fingerprint()), - gossip_timestamp: message_time, - verified_key: None, - verified_key_fingerprint: None, - verifier: None, - secondary_verified_key: None, - secondary_verified_key_fingerprint: None, - secondary_verifier: None, - backward_verified_key_id: None, - fingerprint_changed: false, - } - } - - /// Loads peerstate corresponding to the given address from the database. - pub async fn from_addr(context: &Context, addr: &str) -> Result> { - if context.is_self_addr(addr).await? { - return Ok(None); - } - let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \ - gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \ - verified_key, verified_key_fingerprint, \ - verifier, \ - secondary_verified_key, secondary_verified_key_fingerprint, \ - secondary_verifier, \ - backward_verified_key_id \ - FROM acpeerstates \ - WHERE addr=? COLLATE NOCASE LIMIT 1;"; - Self::from_stmt(context, query, (addr,)).await - } - - /// Loads peerstate corresponding to the given fingerprint from the database. - pub async fn from_fingerprint( - context: &Context, - fingerprint: &Fingerprint, - ) -> Result> { - // NOTE: If it's our key fingerprint, this returns None currently. - let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \ - gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \ - verified_key, verified_key_fingerprint, \ - verifier, \ - secondary_verified_key, secondary_verified_key_fingerprint, \ - secondary_verifier, \ - backward_verified_key_id \ - FROM acpeerstates \ - WHERE public_key_fingerprint=? \ - OR gossip_key_fingerprint=? \ - ORDER BY public_key_fingerprint=? DESC LIMIT 1;"; - let fp = fingerprint.hex(); - Self::from_stmt(context, query, (&fp, &fp, &fp)).await - } - - /// Loads peerstate by address or verified fingerprint. - /// - /// If the address is different but verified fingerprint is the same, - /// peerstate with corresponding verified fingerprint is preferred. - pub async fn from_verified_fingerprint_or_addr( - context: &Context, - fingerprint: &Fingerprint, - addr: &str, - ) -> Result> { - if context.is_self_addr(addr).await? { - return Ok(None); - } - let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \ - gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \ - verified_key, verified_key_fingerprint, \ - verifier, \ - secondary_verified_key, secondary_verified_key_fingerprint, \ - secondary_verifier, \ - backward_verified_key_id \ - FROM acpeerstates \ - WHERE verified_key_fingerprint=? \ - OR addr=? COLLATE NOCASE \ - ORDER BY verified_key_fingerprint=? DESC, addr=? COLLATE NOCASE DESC, \ - last_seen DESC LIMIT 1;"; - let fp = fingerprint.hex(); - Self::from_stmt(context, query, (&fp, addr, &fp, addr)).await - } - - async fn from_stmt( - context: &Context, - query: &str, - params: impl rusqlite::Params + Send, - ) -> Result> { - let peerstate = context - .sql - .query_row_optional(query, params, |row| { - let res = Peerstate { - addr: row.get("addr")?, - last_seen: row.get("last_seen")?, - last_seen_autocrypt: row.get("last_seen_autocrypt")?, - prefer_encrypt: EncryptPreference::from_i32(row.get("prefer_encrypted")?) - .unwrap_or_default(), - public_key: row - .get("public_key") - .ok() - .and_then(|blob: Vec| SignedPublicKey::from_slice(&blob).ok()), - public_key_fingerprint: row - .get::<_, Option>("public_key_fingerprint")? - .map(|s| s.parse::()) - .transpose() - .unwrap_or_default(), - gossip_key: row - .get("gossip_key") - .ok() - .and_then(|blob: Vec| SignedPublicKey::from_slice(&blob).ok()), - gossip_key_fingerprint: row - .get::<_, Option>("gossip_key_fingerprint")? - .map(|s| s.parse::()) - .transpose() - .unwrap_or_default(), - gossip_timestamp: row.get("gossip_timestamp")?, - verified_key: row - .get("verified_key") - .ok() - .and_then(|blob: Vec| SignedPublicKey::from_slice(&blob).ok()), - verified_key_fingerprint: row - .get::<_, Option>("verified_key_fingerprint")? - .map(|s| s.parse::()) - .transpose() - .unwrap_or_default(), - verifier: { - let verifier: Option = row.get("verifier")?; - verifier.filter(|s| !s.is_empty()) - }, - secondary_verified_key: row - .get("secondary_verified_key") - .ok() - .and_then(|blob: Vec| SignedPublicKey::from_slice(&blob).ok()), - secondary_verified_key_fingerprint: row - .get::<_, Option>("secondary_verified_key_fingerprint")? - .map(|s| s.parse::()) - .transpose() - .unwrap_or_default(), - secondary_verifier: { - let secondary_verifier: Option = row.get("secondary_verifier")?; - secondary_verifier.filter(|s| !s.is_empty()) - }, - backward_verified_key_id: row.get("backward_verified_key_id")?, - fingerprint_changed: false, - }; - - Ok(res) - }) - .await?; - Ok(peerstate) - } - - /// Re-calculate `self.public_key_fingerprint` and `self.gossip_key_fingerprint`. - /// If one of them was changed, `self.fingerprint_changed` is set to `true`. - /// - /// Call this after you changed `self.public_key` or `self.gossip_key`. - pub fn recalc_fingerprint(&mut self) { - if let Some(ref public_key) = self.public_key { - let old_public_fingerprint = self.public_key_fingerprint.take(); - self.public_key_fingerprint = Some(public_key.dc_fingerprint()); - - if old_public_fingerprint.is_some() - && old_public_fingerprint != self.public_key_fingerprint - { - self.fingerprint_changed = true; - } - } - - if let Some(ref gossip_key) = self.gossip_key { - let old_gossip_fingerprint = self.gossip_key_fingerprint.take(); - self.gossip_key_fingerprint = Some(gossip_key.dc_fingerprint()); - - if old_gossip_fingerprint.is_none() - || self.gossip_key_fingerprint.is_none() - || old_gossip_fingerprint != self.gossip_key_fingerprint - { - // Warn about gossip key change only if there is no public key obtained from - // Autocrypt header, which overrides gossip key. - if old_gossip_fingerprint.is_some() && self.public_key_fingerprint.is_none() { - self.fingerprint_changed = true; - } - } - } - } - - /// Reset Autocrypt peerstate. - /// - /// Used when it is detected that the contact no longer uses Autocrypt. - pub fn degrade_encryption(&mut self, message_time: i64) { - self.prefer_encrypt = EncryptPreference::Reset; - self.last_seen = message_time; - } - - /// Updates peerstate according to the given `Autocrypt` header. - pub fn apply_header(&mut self, context: &Context, header: &Aheader, message_time: i64) { - if !addr_cmp(&self.addr, &header.addr) { - return; - } - - if message_time >= self.last_seen { - self.last_seen = message_time; - self.last_seen_autocrypt = message_time; - if (header.prefer_encrypt == EncryptPreference::Mutual - || header.prefer_encrypt == EncryptPreference::NoPreference) - && header.prefer_encrypt != self.prefer_encrypt - { - self.prefer_encrypt = header.prefer_encrypt; - } - - if self.public_key.as_ref() != Some(&header.public_key) { - self.public_key = Some(header.public_key.clone()); - self.recalc_fingerprint(); - } - } else { - warn!( - context, - "Ignoring outdated Autocrypt header because message_time={} < last_seen={}.", - message_time, - self.last_seen - ); - } - } - - /// Updates peerstate according to the given `Autocrypt-Gossip` header. - pub fn apply_gossip(&mut self, gossip_header: &Aheader, message_time: i64) { - if self.addr.to_lowercase() != gossip_header.addr.to_lowercase() { - return; - } - - if message_time >= self.gossip_timestamp { - self.gossip_timestamp = message_time; - if self.gossip_key.as_ref() != Some(&gossip_header.public_key) { - self.gossip_key = Some(gossip_header.public_key.clone()); - self.recalc_fingerprint(); - } - - // This is non-standard. - // - // According to Autocrypt 1.1.0 gossip headers SHOULD NOT - // contain encryption preference, but we include it into - // Autocrypt-Gossip and apply it one way (from - // "nopreference" to "mutual"). - // - // This is compatible to standard clients, because they - // can't distinguish it from the case where we have - // contacted the client in the past and received this - // preference via Autocrypt header. - if self.last_seen_autocrypt == 0 - && self.prefer_encrypt == EncryptPreference::NoPreference - && gossip_header.prefer_encrypt == EncryptPreference::Mutual - { - self.prefer_encrypt = EncryptPreference::Mutual; - } - }; - } - - /// Converts the peerstate into the contact public key. - /// - /// Similar to [`Self::peek_key`], but consumes the peerstate and returns owned key. - pub fn take_key(mut self, verified: bool) -> Option { - if verified { - self.verified_key.take() - } else { - self.public_key.take().or_else(|| self.gossip_key.take()) - } - } - - /// Returns a reference to the contact public key. - /// - /// `verified` determines the required verification status of the key. - /// If verified key is requested, returns the verified key, - /// otherwise returns the Autocrypt key. - /// - /// Returned key is suitable for sending in `Autocrypt-Gossip` header. - /// - /// Returns `None` if there is no suitable public key. - pub fn peek_key(&self, verified: bool) -> Option<&SignedPublicKey> { - if verified { - self.verified_key.as_ref() - } else { - self.public_key.as_ref().or(self.gossip_key.as_ref()) - } - } - - /// Set this peerstate to verified; - /// make sure to call `self.save_to_db` to save these changes. - /// - /// Params: - /// - /// * key: The new verified key. - /// * fingerprint: Only set to verified if the key's fingerprint matches this. - /// * verifier: - /// The address which introduces the given contact. - /// If we are verifying the contact, use that contacts address. - pub fn set_verified( - &mut self, - key: SignedPublicKey, - fingerprint: Fingerprint, - verifier: String, - ) -> Result<()> { - if key.dc_fingerprint() == fingerprint { - self.verified_key = Some(key); - self.verified_key_fingerprint = Some(fingerprint); - self.verifier = Some(verifier); - Ok(()) - } else { - Err(Error::msg(format!( - "{fingerprint} is not peer's key fingerprint", - ))) - } - } - - /// Sets the gossiped key as the secondary verified key. - /// - /// If gossiped key is the same as the current verified key, - /// do nothing to avoid overwriting secondary verified key - /// which may be different. - pub fn set_secondary_verified_key(&mut self, gossip_key: SignedPublicKey, verifier: String) { - let fingerprint = gossip_key.dc_fingerprint(); - if self.verified_key_fingerprint.as_ref() != Some(&fingerprint) { - self.secondary_verified_key = Some(gossip_key); - self.secondary_verified_key_fingerprint = Some(fingerprint); - self.secondary_verifier = Some(verifier); - } - } - - /// Saves the peerstate to the database. - pub async fn save_to_db(&self, sql: &Sql) -> Result<()> { - self.save_to_db_ex(sql, None).await - } - - /// Saves the peerstate to the database. - /// - /// * `old_addr`: Old address of the peerstate in case of an AEAP transition. - pub(crate) async fn save_to_db_ex(&self, sql: &Sql, old_addr: Option<&str>) -> Result<()> { - let trans_fn = |t: &mut rusqlite::Transaction| { - let verified_key_fingerprint = - self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()); - if let Some(old_addr) = old_addr { - // We are doing an AEAP transition to the new address and the SQL INSERT below will - // save the existing peerstate as belonging to this new address. We now need to - // "unverify" the peerstate that belongs to the current address in case if the - // contact later wants to move back to the current address. Otherwise the old entry - // will be just found and updated instead of doing AEAP. We can't just delete the - // existing peerstate as this would break encryption to it. This is critical for - // non-verified groups -- if we can't encrypt to the old address, we can't securely - // remove it from the group (to add the new one instead). - // - // NB: We check that `verified_key_fingerprint` hasn't changed to protect from - // possible races. - t.execute( - "UPDATE acpeerstates - SET verified_key=NULL, verified_key_fingerprint='', verifier='' - WHERE addr=? AND verified_key_fingerprint=?", - (old_addr, &verified_key_fingerprint), - )?; - } - t.execute( - "INSERT INTO acpeerstates ( - last_seen, - last_seen_autocrypt, - prefer_encrypted, - public_key, - gossip_timestamp, - gossip_key, - public_key_fingerprint, - gossip_key_fingerprint, - verified_key, - verified_key_fingerprint, - verifier, - secondary_verified_key, - secondary_verified_key_fingerprint, - secondary_verifier, - backward_verified_key_id, - addr) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) - ON CONFLICT (addr) - DO UPDATE SET - last_seen = excluded.last_seen, - last_seen_autocrypt = excluded.last_seen_autocrypt, - prefer_encrypted = excluded.prefer_encrypted, - public_key = excluded.public_key, - gossip_timestamp = excluded.gossip_timestamp, - gossip_key = excluded.gossip_key, - public_key_fingerprint = excluded.public_key_fingerprint, - gossip_key_fingerprint = excluded.gossip_key_fingerprint, - verified_key = excluded.verified_key, - verified_key_fingerprint = excluded.verified_key_fingerprint, - verifier = excluded.verifier, - secondary_verified_key = excluded.secondary_verified_key, - secondary_verified_key_fingerprint = excluded.secondary_verified_key_fingerprint, - secondary_verifier = excluded.secondary_verifier, - backward_verified_key_id = excluded.backward_verified_key_id", - ( - self.last_seen, - self.last_seen_autocrypt, - self.prefer_encrypt as i64, - self.public_key.as_ref().map(|k| k.to_bytes()), - self.gossip_timestamp, - self.gossip_key.as_ref().map(|k| k.to_bytes()), - self.public_key_fingerprint.as_ref().map(|fp| fp.hex()), - self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()), - self.verified_key.as_ref().map(|k| k.to_bytes()), - &verified_key_fingerprint, - self.verifier.as_deref().unwrap_or(""), - self.secondary_verified_key.as_ref().map(|k| k.to_bytes()), - self.secondary_verified_key_fingerprint - .as_ref() - .map(|fp| fp.hex()), - self.secondary_verifier.as_deref().unwrap_or(""), - self.backward_verified_key_id, - &self.addr, - ), - )?; - Ok(()) - }; - sql.transaction(trans_fn).await - } - - /// Returns the address that verified the contact - pub fn get_verifier(&self) -> Option<&str> { - self.verifier.as_deref() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::test_utils::alice_keypair; - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_peerstate_save_to_db() { - let ctx = crate::test_utils::TestContext::new().await; - let addr = "hello@mail.com"; - - let pub_key = alice_keypair().public; - - let peerstate = Peerstate { - addr: addr.into(), - last_seen: 10, - last_seen_autocrypt: 11, - prefer_encrypt: EncryptPreference::Mutual, - public_key: Some(pub_key.clone()), - public_key_fingerprint: Some(pub_key.dc_fingerprint()), - gossip_key: Some(pub_key.clone()), - gossip_timestamp: 12, - gossip_key_fingerprint: Some(pub_key.dc_fingerprint()), - verified_key: Some(pub_key.clone()), - verified_key_fingerprint: Some(pub_key.dc_fingerprint()), - verifier: None, - secondary_verified_key: None, - secondary_verified_key_fingerprint: None, - secondary_verifier: None, - backward_verified_key_id: None, - fingerprint_changed: false, - }; - - assert!( - peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(), - "failed to save to db" - ); - - let peerstate_new = Peerstate::from_addr(&ctx.ctx, addr) - .await - .expect("failed to load peerstate from db") - .expect("no peerstate found in the database"); - - assert_eq!(peerstate, peerstate_new); - let peerstate_new2 = Peerstate::from_fingerprint(&ctx.ctx, &pub_key.dc_fingerprint()) - .await - .expect("failed to load peerstate from db") - .expect("no peerstate found in the database"); - assert_eq!(peerstate, peerstate_new2); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_peerstate_double_create() { - let ctx = crate::test_utils::TestContext::new().await; - let addr = "hello@mail.com"; - let pub_key = alice_keypair().public; - - let peerstate = Peerstate { - addr: addr.into(), - last_seen: 10, - last_seen_autocrypt: 11, - prefer_encrypt: EncryptPreference::Mutual, - public_key: Some(pub_key.clone()), - public_key_fingerprint: Some(pub_key.dc_fingerprint()), - gossip_key: None, - gossip_timestamp: 12, - gossip_key_fingerprint: None, - verified_key: None, - verified_key_fingerprint: None, - verifier: None, - secondary_verified_key: None, - secondary_verified_key_fingerprint: None, - secondary_verifier: None, - backward_verified_key_id: None, - fingerprint_changed: false, - }; - - assert!( - peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(), - "failed to save" - ); - assert!( - peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(), - "double-call with create failed" - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_peerstate_with_empty_gossip_key_save_to_db() { - let ctx = crate::test_utils::TestContext::new().await; - let addr = "hello@mail.com"; - - let pub_key = alice_keypair().public; - - let peerstate = Peerstate { - addr: addr.into(), - last_seen: 10, - last_seen_autocrypt: 11, - prefer_encrypt: EncryptPreference::Mutual, - public_key: Some(pub_key.clone()), - public_key_fingerprint: Some(pub_key.dc_fingerprint()), - gossip_key: None, - gossip_timestamp: 12, - gossip_key_fingerprint: None, - verified_key: None, - verified_key_fingerprint: None, - verifier: None, - secondary_verified_key: None, - secondary_verified_key_fingerprint: None, - secondary_verifier: None, - backward_verified_key_id: None, - fingerprint_changed: false, - }; - - assert!( - peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(), - "failed to save" - ); - - let peerstate_new = Peerstate::from_addr(&ctx.ctx, addr) - .await - .expect("failed to load peerstate from db"); - - assert_eq!(Some(peerstate), peerstate_new); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_peerstate_load_db_defaults() { - let ctx = crate::test_utils::TestContext::new().await; - let addr = "hello@mail.com"; - - // Old code created peerstates with this code and updated - // other values later. If UPDATE failed, other columns had - // default values, in particular fingerprints were set to - // empty strings instead of NULL. This should not be the case - // anymore, but the regression test still checks that defaults - // can be loaded without errors. - ctx.ctx - .sql - .execute("INSERT INTO acpeerstates (addr) VALUES(?)", (addr,)) - .await - .expect("Failed to write to the database"); - - let peerstate = Peerstate::from_addr(&ctx.ctx, addr) - .await - .expect("Failed to load peerstate from db") - .expect("Loaded peerstate is empty"); - - // Check that default values for fingerprints are treated like - // NULL. - assert_eq!(peerstate.public_key_fingerprint, None); - assert_eq!(peerstate.gossip_key_fingerprint, None); - assert_eq!(peerstate.verified_key_fingerprint, None); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_peerstate_degrade_reordering() { - let ctx = crate::test_utils::TestContext::new().await; - - let addr = "example@example.org"; - let pub_key = alice_keypair().public; - let header = Aheader::new(addr.to_string(), pub_key, EncryptPreference::Mutual); - - let mut peerstate = Peerstate { - addr: addr.to_string(), - last_seen: 0, - last_seen_autocrypt: 0, - prefer_encrypt: EncryptPreference::NoPreference, - public_key: None, - public_key_fingerprint: None, - gossip_key: None, - gossip_timestamp: 0, - gossip_key_fingerprint: None, - verified_key: None, - verified_key_fingerprint: None, - verifier: None, - secondary_verified_key: None, - secondary_verified_key_fingerprint: None, - secondary_verifier: None, - backward_verified_key_id: None, - fingerprint_changed: false, - }; - - peerstate.apply_header(&ctx, &header, 100); - assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual); - - peerstate.degrade_encryption(300); - assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Reset); - - // This has message time 200, while encryption was degraded at timestamp 300. - // Because of reordering, header should not be applied. - peerstate.apply_header(&ctx, &header, 200); - assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Reset); - - // Same header will be applied in the future. - peerstate.apply_header(&ctx, &header, 300); - assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual); - } -} diff --git a/src/securejoin/securejoin_tests.rs b/src/securejoin/securejoin_tests.rs index 04f2886464..1c12019688 100644 --- a/src/securejoin/securejoin_tests.rs +++ b/src/securejoin/securejoin_tests.rs @@ -5,7 +5,6 @@ use crate::chat::{remove_contact_from_chat, CantSendReason}; use crate::chatlist::Chatlist; use crate::constants::{self, Chattype}; use crate::imex::{imex, ImexMode}; -use crate::peerstate::Peerstate; use crate::receive_imf::receive_imf; use crate::stock_str::{self, chat_protection_enabled}; use crate::test_utils::{ @@ -351,27 +350,7 @@ async fn test_setup_contact_bob_knows_alice() -> Result<()> { let bob = tcm.bob().await; // Ensure Bob knows Alice_FP - let alice_pubkey = load_self_public_key(&alice.ctx).await?; - let peerstate = Peerstate { - addr: "alice@example.org".into(), - last_seen: 10, - last_seen_autocrypt: 10, - prefer_encrypt: EncryptPreference::Mutual, - public_key: Some(alice_pubkey.clone()), - public_key_fingerprint: Some(alice_pubkey.dc_fingerprint()), - gossip_key: Some(alice_pubkey.clone()), - gossip_timestamp: 10, - gossip_key_fingerprint: Some(alice_pubkey.dc_fingerprint()), - verified_key: None, - verified_key_fingerprint: None, - verifier: None, - secondary_verified_key: None, - secondary_verified_key_fingerprint: None, - secondary_verifier: None, - backward_verified_key_id: None, - fingerprint_changed: false, - }; - peerstate.save_to_db(&bob.ctx.sql).await?; + let alice_contact_id = bob.add_or_lookup_contact_id(alice).await; // Step 1: Generate QR-code, ChatId(0) indicates setup-contact let qr = get_securejoin_qr(&alice.ctx, None).await?; @@ -389,11 +368,6 @@ async fn test_setup_contact_bob_knows_alice() -> Result<()> { contact_id, progress, } => { - let alice_contact_id = - Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown) - .await - .expect("Error looking up contact") - .expect("Contact not found"); assert_eq!(contact_id, alice_contact_id); assert_eq!(progress, 400); } diff --git a/src/tests/aeap.rs b/src/tests/aeap.rs index a1863690ab..c1cfe1c06d 100644 --- a/src/tests/aeap.rs +++ b/src/tests/aeap.rs @@ -5,7 +5,6 @@ use crate::contact; use crate::contact::Contact; use crate::contact::ContactId; use crate::message::Message; -use crate::peerstate::Peerstate; use crate::receive_imf::receive_imf; use crate::securejoin::get_securejoin_qr; use crate::stock_str; @@ -225,15 +224,6 @@ async fn check_aeap_transition( .await; check_no_transition_done(&groups[0..2], "alice@example.org", &bob).await; - // Assert that the autocrypt header is also applied to the peerstate - // if the address changed - let bob_alice_peerstate = Peerstate::from_addr(&bob, ALICE_NEW_ADDR) - .await - .unwrap() - .unwrap(); - assert_eq!(bob_alice_peerstate.last_seen, sent_timestamp); - assert_eq!(bob_alice_peerstate.last_seen_autocrypt, sent_timestamp); - tcm.section("Test switching back"); tcm.change_addr(&alice, "alice@example.org").await; let sent = alice From 0db40d90776f72548ea4fc74fe18ea2f43d15fe8 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 13 Apr 2025 05:34:22 +0000 Subject: [PATCH 056/381] cleanup --- src/mimeparser.rs | 2 +- src/receive_imf/receive_imf_tests.rs | 1 - src/securejoin.rs | 1 - src/securejoin/securejoin_tests.rs | 2 +- 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/mimeparser.rs b/src/mimeparser.rs index dd316f56b8..2ae7032cab 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -13,7 +13,7 @@ use format_flowed::unformat_flowed; use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo}; use mime::Mime; -use crate::aheader::{Aheader, EncryptPreference}; +use crate::aheader::Aheader; use crate::authres::handle_authres; use crate::blob::BlobObject; use crate::chat::ChatId; diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index e8ebfd040b..9ecca541ef 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -4,7 +4,6 @@ use std::time::Duration; use tokio::fs; use super::*; -use crate::aheader::EncryptPreference; use crate::chat::{ add_contact_to_chat, add_to_chat_contacts_table, create_group_chat, get_chat_contacts, get_chat_msgs, is_contact_in_chat, remove_contact_from_chat, send_text_msg, ChatItem, diff --git a/src/securejoin.rs b/src/securejoin.rs index b9f9cf6605..1751e341b7 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -4,7 +4,6 @@ use anyhow::{ensure, Context as _, Error, Result}; use deltachat_contact_tools::ContactAddress; use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; -use crate::aheader::EncryptPreference; use crate::chat::{self, get_chat_id_by_grpid, Chat, ChatId, ChatIdBlocked, ProtectionStatus}; use crate::chatlist_events; use crate::config::Config; diff --git a/src/securejoin/securejoin_tests.rs b/src/securejoin/securejoin_tests.rs index 1c12019688..122e2ca58d 100644 --- a/src/securejoin/securejoin_tests.rs +++ b/src/securejoin/securejoin_tests.rs @@ -350,7 +350,7 @@ async fn test_setup_contact_bob_knows_alice() -> Result<()> { let bob = tcm.bob().await; // Ensure Bob knows Alice_FP - let alice_contact_id = bob.add_or_lookup_contact_id(alice).await; + let alice_contact_id = bob.add_or_lookup_contact_id(&alice).await; // Step 1: Generate QR-code, ChatId(0) indicates setup-contact let qr = get_securejoin_qr(&alice.ctx, None).await?; From 1926cbdf4e6f81125267f8cb07761f03a67c813e Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 13 Apr 2025 16:25:35 +0000 Subject: [PATCH 057/381] self is not a missing address --- src/mimefactory.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 033be389d0..e0dd1226a1 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -315,7 +315,7 @@ impl MimeFactory { if let Some(certificate) = certificate_opt { certificates.push((addr.clone(), certificate)) - } else { + } else if id != ContactId::SELF { missing_key_addresses.insert(addr.clone()); if is_encrypted { warn!(context, "Missing key for {addr}"); From c9a58aebf6f63ba327b938fb81c95c34a12a9ace Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 13 Apr 2025 16:25:35 +0000 Subject: [PATCH 058/381] cleanup --- src/mimefactory.rs | 2 +- src/mimeparser.rs | 2 -- src/qr.rs | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index e0dd1226a1..aea4bedb78 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1047,7 +1047,7 @@ impl MimeFactory { encryption_keyring.extend( encryption_certificates .iter() - .map(|(addr, key)| (*key).clone()), + .map(|(_addr, key)| (*key).clone()), ); // XXX: additional newline is needed diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 2ae7032cab..8a32398838 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -495,7 +495,6 @@ impl MimeMessage { let gossip_headers = mail.headers.get_all_values("Autocrypt-Gossip"); gossiped_keys = update_gossip_peerstates( context, - timestamp_sent, &from.addr, &recipients, gossip_headers, @@ -1882,7 +1881,6 @@ fn remove_header( /// Returns the set of mail recipient addresses for which valid gossip headers were found. async fn update_gossip_peerstates( context: &Context, - message_time: i64, from: &str, recipients: &[SingleInfo], gossip_headers: Vec, diff --git a/src/qr.rs b/src/qr.rs index e2b9648332..991a638a62 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -13,7 +13,6 @@ use serde::Deserialize; pub(crate) use self::dclogin_scheme::configure_from_login_qr; use crate::chat::ChatIdBlocked; use crate::config::Config; -use crate::constants::Blocked; use crate::contact::{Contact, ContactId, Origin}; use crate::context::Context; use crate::events::EventType; From e97513c59bf2d6467b3450df6d4a106f4424898d Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 13 Apr 2025 16:25:35 +0000 Subject: [PATCH 059/381] fix group leaving test --- src/chat/chat_tests.rs | 6 +++--- src/mimeparser.rs | 15 +++++++-------- src/receive_imf.rs | 43 ++++++++++++++++++++++++++---------------- 3 files changed, 37 insertions(+), 27 deletions(-) diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 0161c35953..e1e36a08cf 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -702,12 +702,12 @@ async fn test_leave_group() -> Result<()> { let alice = tcm.alice().await; let bob = tcm.bob().await; - // Create group chat with Bob. + tcm.section("Alice creates group chat with Bob."); let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?; let bob_contact = alice.add_or_lookup_contact(&bob).await.id; add_contact_to_chat(&alice, alice_chat_id, bob_contact).await?; - // Alice sends first message to group. + tcm.section("Alice sends first message to group."); let sent_msg = alice.send_text(alice_chat_id, "Hello!").await; let bob_msg = bob.recv_msg(&sent_msg).await; @@ -720,7 +720,7 @@ async fn test_leave_group() -> Result<()> { // Shift the time so that we can later check the 'Group left' message's timestamp: SystemTime::shift(Duration::from_secs(60)); - // Bob leaves the group. + tcm.section("Bob leaves the group."); let bob_chat_id = bob_msg.chat_id; bob_chat_id.accept(&bob).await?; remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?; diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 8a32398838..8fe5cd6ce2 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -493,13 +493,9 @@ impl MimeMessage { // but only if the mail was correctly signed. Probably it's ok to not require // encryption here, but let's follow the standard. let gossip_headers = mail.headers.get_all_values("Autocrypt-Gossip"); - gossiped_keys = update_gossip_peerstates( - context, - &from.addr, - &recipients, - gossip_headers, - ) - .await?; + gossiped_keys = + update_gossip_peerstates(context, &from.addr, &recipients, gossip_headers) + .await?; } if let Some(inner_from) = inner_from { @@ -1443,7 +1439,10 @@ impl MimeMessage { }; let key = match SignedPublicKey::from_asc(key) { Err(err) => { - warn!(context, "PGP key attachment is not an ASCII-armored file: {err:#}."); + warn!( + context, + "PGP key attachment is not an ASCII-armored file: {err:#}." + ); return Ok(false); } Ok((key, _)) => key, diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 22c2bf51a4..55feaf1472 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -2399,7 +2399,7 @@ async fn apply_group_changes( } if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) { - removed_id = Contact::lookup_id_by_addr(context, removed_addr, Origin::Unknown).await?; + removed_id = lookup_pgp_contact_by_address(context, removed_addr, chat_id).await?; if let Some(id) = removed_id { better_msg = if id == from_id { silent = true; @@ -2411,6 +2411,8 @@ async fn apply_group_changes( warn!(context, "Removed {removed_addr:?} has no contact id.") } } else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) { + // TODO: lookup PGP contact. + // There must be a gossip header. if let Some(contact_id) = Contact::lookup_id_by_addr(context, added_addr, Origin::Unknown).await? { @@ -3175,6 +3177,29 @@ async fn add_or_lookup_pgp_contacts_by_address_list( Ok(contact_ids) } +async fn lookup_pgp_contact_by_address( + context: &Context, + addr: &str, + chat_id: ChatId, +) -> Result> { + let contact_id: Option = context + .sql + .query_row_optional( + "SELECT id FROM contacts + WHERE contacts.addr=? + AND EXISTS (SELECT 1 FROM chats_contacts + WHERE contact_id=contacts.id + AND chat_id=?)", + (addr, chat_id), + |row| { + let contact_id: ContactId = row.get(0)?; + Ok(contact_id) + }, + ) + .await?; + Ok(contact_id) +} + /// Looks up PGP-contacts by email addresses. /// /// This is used as a fallback when email addresses are available, @@ -3198,21 +3223,7 @@ async fn lookup_pgp_contacts_by_address_list( for info in address_list { let addr = &info.addr; - let contact_id = context - .sql - .query_row_optional( - "SELECT id FROM contacts - WHERE contacts.addr=? - AND EXISTS (SELECT 1 FROM chats_contacts - WHERE contact_id=contacts.id - AND chat_id=?)", - (addr, chat_id), - |row| { - let contact_id: ContactId = row.get(0)?; - Ok(contact_id) - }, - ) - .await?; + let contact_id = lookup_pgp_contact_by_address(context, addr, chat_id).await?; contact_ids.push(contact_id); } debug_assert_eq!(address_list.len(), contact_ids.len()); From 8093f61b6c975fe6507d95cb8faa9ce7fd4b4cc7 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 13 Apr 2025 19:50:35 +0000 Subject: [PATCH 060/381] member added fixes --- src/chat/chat_tests.rs | 6 +++--- src/mimefactory.rs | 2 ++ src/receive_imf.rs | 34 +++++++++++++++++++++++++++------- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index e1e36a08cf..59d0e5dc96 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -320,7 +320,7 @@ async fn test_member_add_remove() -> Result<()> { assert_eq!(alice_bob_contact.get_display_name(), "robert"); } - // Create and promote a group. + tcm.section("Create and promote a group."); let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?; let alice_fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await; @@ -330,7 +330,7 @@ async fn test_member_add_remove() -> Result<()> { .await; let fiona_chat_id = fiona.recv_msg(&sent).await.chat_id; - // Alice adds Bob to the chat. + tcm.section("Alice adds Bob to the chat."); add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?; let sent = alice.pop_sent_msg().await; fiona.recv_msg(&sent).await; @@ -349,7 +349,7 @@ async fn test_member_add_remove() -> Result<()> { assert!(contact.is_pgp_contact()); } - // Alice removes Bob from the chat. + tcm.section("Alice removes Bob from the chat."); remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?; let sent = alice.pop_sent_msg().await; assert!(!sent.payload.contains("robert")); diff --git a/src/mimefactory.rs b/src/mimefactory.rs index aea4bedb78..db1ecce1a9 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1283,6 +1283,8 @@ impl MimeFactory { } } SystemMessage::MemberAddedToGroup => { + // TODO: lookup the contact by ID rather than email address. + // We are adding PGP contacts, the cannot be looked up by address. let email_to_add = msg.param.get(Param::Arg).unwrap_or_default(); placeholdertext = Some(stock_str::msg_add_member_remote(context, email_to_add).await); diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 55feaf1472..ccd2954cb3 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -2411,14 +2411,15 @@ async fn apply_group_changes( warn!(context, "Removed {removed_addr:?} has no contact id.") } } else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) { - // TODO: lookup PGP contact. - // There must be a gossip header. - if let Some(contact_id) = - Contact::lookup_id_by_addr(context, added_addr, Origin::Unknown).await? - { - added_id = Some(contact_id); + if let Some(key) = mime_parser.gossiped_keys.get(added_addr) { + let fingerprint = key.dc_fingerprint().hex(); + if let Some(contact_id) = lookup_pgp_contact_by_fingerprint(context, &fingerprint).await? { + added_id = Some(contact_id); + } else { + warn!(context, "Added {added_addr:?} has no contact id."); + } } else { - warn!(context, "Added {added_addr:?} has no contact id."); + warn!(context, "Added {added_addr:?} has no gossiped key."); } better_msg = Some(stock_str::msg_add_member_local(context, added_addr, from_id).await); @@ -3200,6 +3201,25 @@ async fn lookup_pgp_contact_by_address( Ok(contact_id) } +async fn lookup_pgp_contact_by_fingerprint( + context: &Context, + fingerprint: &str, +) -> Result> { + let contact_id: Option = context + .sql + .query_row_optional( + "SELECT id FROM contacts + WHERE contacts.fingerprint=?", + (fingerprint,), + |row| { + let contact_id: ContactId = row.get(0)?; + Ok(contact_id) + }, + ) + .await?; + Ok(contact_id) +} + /// Looks up PGP-contacts by email addresses. /// /// This is used as a fallback when email addresses are available, From 77237e350e27bac7f5f9466bd7a877eaa9481517 Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 14 Apr 2025 16:43:23 +0000 Subject: [PATCH 061/381] chat::chat_tests::test_member_add_remove is fixed --- src/chat.rs | 13 ++++------- src/param.rs | 5 ++++ src/receive_imf.rs | 26 ++++++++++----------- src/receive_imf/receive_imf_tests.rs | 4 ++-- src/stock_str.rs | 34 ++++++++-------------------- src/stock_str/stock_str_tests.rs | 25 ++++++++++---------- 6 files changed, 47 insertions(+), 60 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index cada889209..29797802ed 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -3909,8 +3909,8 @@ pub(crate) async fn add_contact_to_chat_ex( if chat.typ == Chattype::Group && chat.is_promoted() { msg.viewtype = Viewtype::Text; - let contact_addr = contact.get_addr().to_lowercase(); - msg.text = stock_str::msg_add_member_local(context, &contact_addr, ContactId::SELF).await; + let contact_addr = contact.get_addr().to_lowercase(); // FIXME contact is not identified by addr + msg.text = stock_str::msg_add_member_local(context, contact.id, ContactId::SELF).await; msg.param.set_cmd(SystemMessage::MemberAddedToGroup); msg.param.set(Param::Arg, contact_addr); msg.param.set_int(Param::Arg2, from_handshake.into()); @@ -4104,12 +4104,9 @@ pub async fn remove_contact_from_chat( if contact_id == ContactId::SELF { msg.text = stock_str::msg_group_left_local(context, ContactId::SELF).await; } else { - msg.text = stock_str::msg_del_member_local( - context, - contact.get_addr(), - ContactId::SELF, - ) - .await; + msg.text = + stock_str::msg_del_member_local(context, contact_id, ContactId::SELF) + .await; } msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup); msg.param.set(Param::Arg, contact.get_addr().to_lowercase()); diff --git a/src/param.rs b/src/param.rs index 9500778a27..8fb5448fc3 100644 --- a/src/param.rs +++ b/src/param.rs @@ -98,6 +98,11 @@ pub enum Param { Cmd = b'S', /// For Messages + /// + /// For "MemberRemovedFromGroup" this is the email address + /// removed from the group. + /// + /// For "MemberAddedToGroup" this is the email address added to the group. Arg = b'E', /// For Messages diff --git a/src/receive_imf.rs b/src/receive_imf.rs index ccd2954cb3..8d5ccc0933 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -2405,7 +2405,7 @@ async fn apply_group_changes( silent = true; Some(stock_str::msg_group_left_local(context, from_id).await) } else { - Some(stock_str::msg_del_member_local(context, removed_addr, from_id).await) + Some(stock_str::msg_del_member_local(context, id, from_id).await) }; } else { warn!(context, "Removed {removed_addr:?} has no contact id.") @@ -2413,16 +2413,18 @@ async fn apply_group_changes( } else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) { if let Some(key) = mime_parser.gossiped_keys.get(added_addr) { let fingerprint = key.dc_fingerprint().hex(); - if let Some(contact_id) = lookup_pgp_contact_by_fingerprint(context, &fingerprint).await? { + if let Some(contact_id) = + lookup_pgp_contact_by_fingerprint(context, &fingerprint).await? + { added_id = Some(contact_id); + better_msg = + Some(stock_str::msg_add_member_local(context, contact_id, from_id).await); } else { warn!(context, "Added {added_addr:?} has no contact id."); } } else { warn!(context, "Added {added_addr:?} has no gossiped key."); } - - better_msg = Some(stock_str::msg_add_member_local(context, added_addr, from_id).await); } let group_name_timestamp = mime_parser @@ -2673,7 +2675,7 @@ async fn group_changes_msgs( removed_ids: &HashSet, chat_id: ChatId, ) -> Result)>> { - let mut group_changes_msgs = Vec::new(); + let mut group_changes_msgs: Vec<(String, SystemMessage, Option)> = Vec::new(); if !added_ids.is_empty() { warn!( context, @@ -2688,21 +2690,17 @@ async fn group_changes_msgs( } group_changes_msgs.reserve(added_ids.len() + removed_ids.len()); for contact_id in added_ids { - let contact = Contact::get_by_id(context, *contact_id).await?; group_changes_msgs.push(( - stock_str::msg_add_member_local(context, contact.get_addr(), ContactId::UNDEFINED) - .await, + stock_str::msg_add_member_local(context, *contact_id, ContactId::UNDEFINED).await, SystemMessage::MemberAddedToGroup, - Some(contact.id), + Some(*contact_id), )); } for contact_id in removed_ids { - let contact = Contact::get_by_id(context, *contact_id).await?; group_changes_msgs.push(( - stock_str::msg_del_member_local(context, contact.get_addr(), ContactId::UNDEFINED) - .await, + stock_str::msg_del_member_local(context, *contact_id, ContactId::UNDEFINED).await, SystemMessage::MemberRemovedFromGroup, - Some(contact.id), + Some(*contact_id), )); } @@ -3210,7 +3208,7 @@ async fn lookup_pgp_contact_by_fingerprint( .query_row_optional( "SELECT id FROM contacts WHERE contacts.fingerprint=?", - (fingerprint,), + (fingerprint,), |row| { let contact_id: ContactId = row.get(0)?; Ok(contact_id) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 9ecca541ef..3d6cecccc7 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -4846,7 +4846,7 @@ async fn test_protected_group_add_remove_member_missing_key() -> Result<()> { assert!(msg.is_info()); assert_eq!( msg.get_text(), - stock_str::msg_add_member_local(alice, &fiona_addr, ContactId::SELF).await + stock_str::msg_add_member_local(alice, alice_fiona_id, ContactId::SELF).await ); remove_contact_from_chat(alice, group_id, alice_bob_id).await?; @@ -4855,7 +4855,7 @@ async fn test_protected_group_add_remove_member_missing_key() -> Result<()> { assert!(msg.is_info()); assert_eq!( msg.get_text(), - stock_str::msg_del_member_local(alice, &bob_addr, ContactId::SELF,).await + stock_str::msg_del_member_local(alice, alice_bob_id, ContactId::SELF).await ); Ok(()) } diff --git a/src/stock_str.rs b/src/stock_str.rs index aca9821413..b4ca59c633 100644 --- a/src/stock_str.rs +++ b/src/stock_str.rs @@ -636,29 +636,22 @@ pub(crate) async fn msg_add_member_remote(context: &Context, added_member_addr: /// contacts to combine with the display name. pub(crate) async fn msg_add_member_local( context: &Context, - added_member_addr: &str, + added_member: ContactId, by_contact: ContactId, ) -> String { - let addr = added_member_addr; - let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await { - Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id) - .await - .map(|contact| contact.get_display_name().to_string()) - .unwrap_or_else(|_| addr.to_string()), - _ => addr.to_string(), - }; + let whom = added_member.get_stock_name(context).await; if by_contact == ContactId::UNDEFINED { translated(context, StockMessage::MsgAddMember) .await - .replace1(whom) + .replace1(&whom) } else if by_contact == ContactId::SELF { translated(context, StockMessage::MsgYouAddMember) .await - .replace1(whom) + .replace1(&whom) } else { translated(context, StockMessage::MsgAddMemberBy) .await - .replace1(whom) + .replace1(&whom) .replace2(&by_contact.get_stock_name(context).await) } } @@ -687,29 +680,22 @@ pub(crate) async fn msg_del_member_remote(context: &Context, removed_member_addr /// the contacts to combine with the display name. pub(crate) async fn msg_del_member_local( context: &Context, - removed_member_addr: &str, + removed_member: ContactId, by_contact: ContactId, ) -> String { - let addr = removed_member_addr; - let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await { - Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id) - .await - .map(|contact| contact.get_display_name().to_string()) - .unwrap_or_else(|_| addr.to_string()), - _ => addr.to_string(), - }; + let whom = removed_member.get_stock_name(context).await; if by_contact == ContactId::UNDEFINED { translated(context, StockMessage::MsgDelMember) .await - .replace1(whom) + .replace1(&whom) } else if by_contact == ContactId::SELF { translated(context, StockMessage::MsgYouDelMember) .await - .replace1(whom) + .replace1(&whom) } else { translated(context, StockMessage::MsgDelMemberBy) .await - .replace1(whom) + .replace1(&whom) .replace2(&by_contact.get_stock_name(context).await) } } diff --git a/src/stock_str/stock_str_tests.rs b/src/stock_str/stock_str_tests.rs index 334927b1af..f64bb6fb6c 100644 --- a/src/stock_str/stock_str_tests.rs +++ b/src/stock_str/stock_str_tests.rs @@ -70,12 +70,15 @@ async fn test_stock_system_msg_simple() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_stock_system_msg_add_member_by_me() { let t = TestContext::new().await; + let alice_contact_id = Contact::create(&t, "Alice", "alice@example.org") + .await + .expect("failed to create contact"); assert_eq!( msg_add_member_remote(&t, "alice@example.org").await, "I added member alice@example.org." ); assert_eq!( - msg_add_member_local(&t, "alice@example.org", ContactId::SELF).await, + msg_add_member_local(&t, alice_contact_id, ContactId::SELF).await, "You added member alice@example.org." ) } @@ -83,7 +86,7 @@ async fn test_stock_system_msg_add_member_by_me() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_stock_system_msg_add_member_by_me_with_displayname() { let t = TestContext::new().await; - Contact::create(&t, "Alice", "alice@example.org") + let alice_contact_id = Contact::create(&t, "Alice", "alice@example.org") .await .expect("failed to create contact"); assert_eq!( @@ -91,7 +94,7 @@ async fn test_stock_system_msg_add_member_by_me_with_displayname() { "I added member alice@example.org." ); assert_eq!( - msg_add_member_local(&t, "alice@example.org", ContactId::SELF).await, + msg_add_member_local(&t, alice_contact_id, ContactId::SELF).await, "You added member Alice." ); } @@ -99,16 +102,14 @@ async fn test_stock_system_msg_add_member_by_me_with_displayname() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_stock_system_msg_add_member_by_other_with_displayname() { let t = TestContext::new().await; - let contact_id = { - Contact::create(&t, "Alice", "alice@example.org") - .await - .expect("Failed to create contact Alice"); - Contact::create(&t, "Bob", "bob@example.com") - .await - .expect("failed to create bob") - }; + let alice_contact_id = Contact::create(&t, "Alice", "alice@example.org") + .await + .expect("Failed to create contact Alice"); + let bob_contact_id = Contact::create(&t, "Bob", "bob@example.com") + .await + .expect("failed to create bob"); assert_eq!( - msg_add_member_local(&t, "alice@example.org", contact_id,).await, + msg_add_member_local(&t, alice_contact_id, bob_contact_id).await, "Member Alice added by Bob." ); } From fac508a06e6b5ebe01913584d691836fdc787c61 Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 14 Apr 2025 17:56:13 +0000 Subject: [PATCH 062/381] fix verification check --- src/receive_imf.rs | 48 +++++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 8d5ccc0933..1c9167cbbd 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -415,7 +415,7 @@ pub(crate) async fn receive_imf_inner( received_msg = None; } - let verified_encryption = has_verified_encryption(&mime_parser, from_id)?; + let verified_encryption = has_verified_encryption(context, &mime_parser, from_id).await?; if verified_encryption == VerifiedEncryption::Verified { mark_recipients_as_verified(context, from_id, &to_ids, &mime_parser).await?; @@ -3001,7 +3001,8 @@ enum VerifiedEncryption { /// Checks whether the message is allowed to appear in a protected chat. /// /// This means that it is encrypted and signed with a verified key. -fn has_verified_encryption( +async fn has_verified_encryption( + context: &Context, mimeparser: &MimeMessage, from_id: ContactId, ) -> Result { @@ -3011,34 +3012,37 @@ fn has_verified_encryption( return Ok(NotVerified("This message is not encrypted".to_string())); }; + if from_id == ContactId::SELF { + // TODO: check that the message is signed with our key? + return Ok(Verified); + } + // ensure, the contact is verified // and the message is signed with a verified key of the sender. // this check is skipped for SELF as there is no proper SELF-peerstate // and results in group-splits otherwise. - if from_id != ContactId::SELF { - // FIXME - /* - let Some(peerstate) = &mimeparser.peerstate else { - return Ok(NotVerified( - "No peerstate, the contact isn't verified".to_string(), - )); - }; + let from_contact = + Contact::get_by_id(context, from_id).await?; - let signed_with_verified_key = peerstate - .verified_key_fingerprint - .as_ref() - .filter(|fp| mimeparser.signatures.contains(fp)) - .is_some(); + let Some(fingerprint) = from_contact.fingerprint() else { + return Ok(NotVerified( + "The message was sent without encryption".to_string(), + )); + }; - if !signed_with_verified_key { - return Ok(NotVerified( - "The message was sent with non-verified encryption".to_string(), - )); - } - */ + if from_contact.get_verifier_id(context).await?.is_none() { + return Ok(NotVerified("The message was sent by non-verified contact.".to_string())); } + let fingerprint: Fingerprint = fingerprint.parse().context("Failed to parse fingerprint")?; - Ok(Verified) + let signed_with_verified_key = mimeparser.signatures.contains(&fingerprint); + if signed_with_verified_key { + Ok(Verified) + } else { + Ok(NotVerified( + "The message was sent with non-verified encryption".to_string(), + )) + } } async fn mark_recipients_as_verified( From bce2771263fadf6fee6dbecd1b1f07fd6d222d86 Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 14 Apr 2025 17:56:13 +0000 Subject: [PATCH 063/381] Add is_pgp_contact to JSON-RPC --- deltachat-jsonrpc/src/api/types/contact.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/deltachat-jsonrpc/src/api/types/contact.rs b/deltachat-jsonrpc/src/api/types/contact.rs index eb85cf35c8..4300f19540 100644 --- a/deltachat-jsonrpc/src/api/types/contact.rs +++ b/deltachat-jsonrpc/src/api/types/contact.rs @@ -19,6 +19,16 @@ pub struct ContactObject { profile_image: Option, // BLOBS name_and_addr: String, is_blocked: bool, + + /// Is the contact a PGP contact. + is_pgp_contact: bool, + + /// Is encryption available for this contact. + /// + /// This can only be true of PGP-contacts. + /// However, it is possible to have a PGP-contact + /// for which encryption is not available because we don't have a key yet, + /// e.g. if we just scanned the fingerprint from a QR code. e2ee_avail: bool, /// True if the contact can be added to verified groups. @@ -80,6 +90,7 @@ impl ContactObject { profile_image, //BLOBS name_and_addr: contact.get_name_n_addr(), is_blocked: contact.is_blocked(), + is_pgp_contact: contact.is_pgp_contact(), e2ee_avail: contact.e2ee_avail(context).await?, is_verified, is_profile_verified, From 35f367dddba4f924ea8a6b6d72439063da5998eb Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 14 Apr 2025 21:28:56 +0000 Subject: [PATCH 064/381] fix tests::verified_chats::test_verified_member_added_reordering --- src/tests/verified_chats.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/verified_chats.rs b/src/tests/verified_chats.rs index aeb46744c8..b360c8cb68 100644 --- a/src/tests/verified_chats.rs +++ b/src/tests/verified_chats.rs @@ -891,7 +891,7 @@ async fn test_verified_member_added_reordering() -> Result<()> { let fiona = &tcm.fiona().await; enable_verified_oneonone_chats(&[alice, bob, fiona]).await; - let alice_fiona_contact_id = Contact::create(alice, "Fiona", "fiona@example.net").await?; + let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await; // Bob and Fiona scan Alice's QR code. tcm.execute_securejoin(bob, alice).await; @@ -920,7 +920,7 @@ async fn test_verified_member_added_reordering() -> Result<()> { let fiona_received_message = fiona.recv_msg(&bob_sent_message).await; assert_eq!( fiona_received_message.get_text(), - "[The message was sent with non-verified encryption. See 'Info' for more details]" + "[The message was sent by non-verified contact. See 'Info' for more details]" ); Ok(()) From c7abc261c219c70be2f5cbae9c773720bbc3124e Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 14 Apr 2025 21:28:56 +0000 Subject: [PATCH 065/381] fix contact::contact_tests::test_import_vcard_key_change --- src/contact/contact_tests.rs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs index 00e5e73a8b..00b03a96ef 100644 --- a/src/contact/contact_tests.rs +++ b/src/contact/contact_tests.rs @@ -1170,16 +1170,16 @@ async fn test_import_vcard_key_change() -> Result<()> { let msg = bob.recv_msg(&sent_msg).await; assert!(msg.get_showpadlock()); - let bob = &TestContext::new().await; - bob.configure_addr(bob_addr).await; - bob.set_config(Config::Displayname, Some("New Bob")).await?; - let avatar_path = bob.dir.path().join("avatar.png"); + let bob1 = &TestContext::new().await; + bob1.configure_addr(bob_addr).await; + bob1.set_config(Config::Displayname, Some("New Bob")).await?; + let avatar_path = bob1.dir.path().join("avatar.png"); let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png"); tokio::fs::write(&avatar_path, avatar_bytes).await?; - bob.set_config(Config::Selfavatar, Some(avatar_path.to_str().unwrap())) + bob1.set_config(Config::Selfavatar, Some(avatar_path.to_str().unwrap())) .await?; SystemTime::shift(Duration::from_secs(1)); - let vcard1 = make_vcard(bob, &[ContactId::SELF]).await?; + let vcard1 = make_vcard(bob1, &[ContactId::SELF]).await?; let alice_bob_id1 = import_vcard(alice, &vcard1).await?[0]; assert_ne!(alice_bob_id1, alice_bob_id); let alice_bob_contact = Contact::get_by_id(alice, alice_bob_id).await?; @@ -1188,14 +1188,18 @@ async fn test_import_vcard_key_change() -> Result<()> { let alice_bob_contact1 = Contact::get_by_id(alice, alice_bob_id1).await?; assert_eq!(alice_bob_contact1.get_authname(), "New Bob"); assert!(alice_bob_contact1.get_profile_image(alice).await?.is_some()); + + // Last message is still the same, + // no new messages are added. let msg = alice.get_last_msg_in(chat_id).await; - assert!(msg.is_info()); assert_eq!( msg.get_text(), - stock_str::contact_setup_changed(alice, bob_addr).await + "moin" ); - let sent_msg = alice.send_text(chat_id, "moin").await; - let msg = bob.recv_msg(&sent_msg).await; + + let chat_id1 = ChatId::create_for_contact(alice, alice_bob_id1).await?; + let sent_msg = alice.send_text(chat_id1, "moin").await; + let msg = bob1.recv_msg(&sent_msg).await; assert!(msg.get_showpadlock()); // The old vCard is imported, but doesn't change Bob's key for Alice. From ebf63b54dd4bc9f92a967ebe6c0e85f68f6710ca Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 15 Apr 2025 04:06:09 +0000 Subject: [PATCH 066/381] fix securejoin::securejoin_tests::test_lost_contact_confirm --- src/securejoin/securejoin_tests.rs | 34 +++++++----------------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/src/securejoin/securejoin_tests.rs b/src/securejoin/securejoin_tests.rs index 122e2ca58d..3f6cc72881 100644 --- a/src/securejoin/securejoin_tests.rs +++ b/src/securejoin/securejoin_tests.rs @@ -686,8 +686,10 @@ async fn test_unknown_sender() -> Result<()> { } /// Tests that Bob gets Alice as verified -/// if `vc-contact-confirm` is lost but Alice then sends -/// a message to Bob in a verified 1:1 chat with a `Chat-Verified` header. +/// if `vc-contact-confirm` is lost. +/// Previously `vc-contact-confirm` was used +/// to confirm backward verification, +/// but backward verification is not tracked anymore. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_lost_contact_confirm() { let mut tcm = TestContextManager::new(); @@ -715,35 +717,15 @@ async fn test_lost_contact_confirm() { alice.recv_msg_trash(&sent).await; // Alice has Bob verified now. - let contact_bob_id = Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown) - .await - .expect("Error looking up contact") - .expect("Contact not found"); - let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id) - .await - .unwrap(); + let contact_bob = alice.add_or_lookup_pgp_contact(&bob) + .await; assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), true); // Alice sends vc-contact-confirm, but it gets lost. let _sent_vc_contact_confirm = alice.pop_sent_msg().await; - // Bob should not yet have Alice verified - let contact_alice_id = Contact::lookup_id_by_addr(&bob, "alice@example.org", Origin::Unknown) - .await - .expect("Error looking up contact") - .expect("Contact not found"); - let contact_alice = Contact::get_by_id(&bob, contact_alice_id).await.unwrap(); - assert_eq!(contact_alice.is_verified(&bob).await.unwrap(), false); - - // Alice sends a text message to Bob. - let received_hello = tcm.send_recv(&alice, &bob, "Hello!").await; - let chat_id = received_hello.chat_id; - let chat = Chat::load_from_db(&bob, chat_id).await.unwrap(); - assert_eq!(chat.is_protected(), true); - - // Received text message in a verified 1:1 chat results in backward verification - // and Bob now marks alice as verified. - let contact_alice = Contact::get_by_id(&bob, contact_alice_id).await.unwrap(); + // Bob has alice as verified too, even though vc-contact-confirm is lost. + let contact_alice = bob.add_or_lookup_pgp_contact(&alice).await; assert_eq!(contact_alice.is_verified(&bob).await.unwrap(), true); } From e2685bbc516272d774b918687f7de44f8e591f56 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 15 Apr 2025 04:11:23 +0000 Subject: [PATCH 067/381] contact::contact_tests::test_synchronize_status --- src/contact/contact_tests.rs | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs index 00b03a96ef..1fe980fd5d 100644 --- a/src/contact/contact_tests.rs +++ b/src/contact/contact_tests.rs @@ -780,21 +780,21 @@ CCCB 5AA9 F6E1 141C 9431 /// synchronized when the message is not encrypted. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_synchronize_status() -> Result<()> { + let mut tcm = TestContextManager::new(); + // Alice has two devices. - let alice1 = TestContext::new_alice().await; - let alice2 = TestContext::new_alice().await; + let alice1 = &tcm.alice().await; + let alice2 = &tcm.alice().await; // Bob has one device. - let bob = TestContext::new_bob().await; + let bob = &tcm.bob().await; let default_status = alice1.get_config(Config::Selfstatus).await?; alice1 .set_config(Config::Selfstatus, Some("New status")) .await?; - let chat = alice1 - .create_chat_with_contact("Bob", "bob@example.net") - .await; + let chat = alice1.create_email_chat(bob).await; // Alice sends a message to Bob from the first device. send_text_msg(&alice1, chat.id, "Hello".to_string()).await?; @@ -813,17 +813,8 @@ async fn test_synchronize_status() -> Result<()> { // Message was not encrypted, so status is not copied. assert_eq!(alice2.get_config(Config::Selfstatus).await?, default_status); - // Bob replies. - let chat = bob - .create_chat_with_contact("Alice", "alice@example.org") - .await; - - send_text_msg(&bob, chat.id, "Reply".to_string()).await?; - let sent_msg = bob.pop_sent_msg().await; - alice1.recv_msg(&sent_msg).await; - alice2.recv_msg(&sent_msg).await; - - // Alice sends second message. + // Alice sends encrypted message. + let chat = alice1.create_chat(bob).await; send_text_msg(&alice1, chat.id, "Hello".to_string()).await?; let sent_msg = alice1.pop_sent_msg().await; From eb0d6f6346176c42e40a9e4af72a8f473edf7ca4 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 15 Apr 2025 04:23:45 +0000 Subject: [PATCH 068/381] contact::contact_tests::test_selfavatar_changed_event --- src/contact/contact_tests.rs | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs index 1fe980fd5d..8016a00cc1 100644 --- a/src/contact/contact_tests.rs +++ b/src/contact/contact_tests.rs @@ -836,12 +836,14 @@ async fn test_synchronize_status() -> Result<()> { /// Tests that DC_EVENT_SELFAVATAR_CHANGED is emitted on avatar changes. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_selfavatar_changed_event() -> Result<()> { + let mut tcm = TestContextManager::new(); + // Alice has two devices. - let alice1 = TestContext::new_alice().await; - let alice2 = TestContext::new_alice().await; + let alice1 = &tcm.alice().await; + let alice2 = &tcm.alice().await; // Bob has one device. - let bob = TestContext::new_bob().await; + let bob = &tcm.bob().await; assert_eq!(alice1.get_config(Config::Selfavatar).await?, None); @@ -857,19 +859,8 @@ async fn test_selfavatar_changed_event() -> Result<()> { .get_matching(|e| matches!(e, EventType::SelfavatarChanged)) .await; - // Bob sends a message so that Alice can encrypt to him. - let chat = bob - .create_chat_with_contact("Alice", "alice@example.org") - .await; - - send_text_msg(&bob, chat.id, "Reply".to_string()).await?; - let sent_msg = bob.pop_sent_msg().await; - alice1.recv_msg(&sent_msg).await; - alice2.recv_msg(&sent_msg).await; - // Alice sends a message. - let alice1_chat_id = alice1.get_last_msg().await.chat_id; - alice1_chat_id.accept(&alice1).await?; + let alice1_chat_id = alice1.create_chat(bob).await.id; send_text_msg(&alice1, alice1_chat_id, "Hello".to_string()).await?; let sent_msg = alice1.pop_sent_msg().await; From 31d279ffc663003dc0a2886b2a224fd9b783273a Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 15 Apr 2025 15:03:19 +0000 Subject: [PATCH 069/381] fix sync::tests::test_unpromoted_group_qr_sync --- src/contact/contact_tests.rs | 8 +++----- src/receive_imf.rs | 24 ++++++++++++++++++------ src/securejoin/securejoin_tests.rs | 3 +-- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs index 8016a00cc1..4d5835a86d 100644 --- a/src/contact/contact_tests.rs +++ b/src/contact/contact_tests.rs @@ -1154,7 +1154,8 @@ async fn test_import_vcard_key_change() -> Result<()> { let bob1 = &TestContext::new().await; bob1.configure_addr(bob_addr).await; - bob1.set_config(Config::Displayname, Some("New Bob")).await?; + bob1.set_config(Config::Displayname, Some("New Bob")) + .await?; let avatar_path = bob1.dir.path().join("avatar.png"); let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png"); tokio::fs::write(&avatar_path, avatar_bytes).await?; @@ -1174,10 +1175,7 @@ async fn test_import_vcard_key_change() -> Result<()> { // Last message is still the same, // no new messages are added. let msg = alice.get_last_msg_in(chat_id).await; - assert_eq!( - msg.get_text(), - "moin" - ); + assert_eq!(msg.get_text(), "moin"); let chat_id1 = ChatId::create_for_contact(alice, alice_bob_id1).await?; let sent_msg = alice.send_text(chat_id1, "moin").await; diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 1c9167cbbd..de92275c66 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -23,6 +23,7 @@ use crate::ephemeral::{stock_ephemeral_timer_changed, Timer as EphemeralTimer}; use crate::events::EventType; use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::imap::{markseen_on_imap_table, GENERATED_PREFIX}; +use crate::key::load_self_public_key_opt; use crate::key::{DcKey, Fingerprint, SignedPublicKey}; use crate::log::LogExt; use crate::message::{ @@ -3021,8 +3022,7 @@ async fn has_verified_encryption( // and the message is signed with a verified key of the sender. // this check is skipped for SELF as there is no proper SELF-peerstate // and results in group-splits otherwise. - let from_contact = - Contact::get_by_id(context, from_id).await?; + let from_contact = Contact::get_by_id(context, from_id).await?; let Some(fingerprint) = from_contact.fingerprint() else { return Ok(NotVerified( @@ -3031,7 +3031,9 @@ async fn has_verified_encryption( }; if from_contact.get_verifier_id(context).await?.is_none() { - return Ok(NotVerified("The message was sent by non-verified contact.".to_string())); + return Ok(NotVerified( + "The message was sent by non-verified contact".to_string(), + )); } let fingerprint: Fingerprint = fingerprint.parse().context("Failed to parse fingerprint")?; @@ -3207,7 +3209,7 @@ async fn lookup_pgp_contact_by_fingerprint( context: &Context, fingerprint: &str, ) -> Result> { - let contact_id: Option = context + if let Some(contact_id) = context .sql .query_row_optional( "SELECT id FROM contacts @@ -3218,8 +3220,18 @@ async fn lookup_pgp_contact_by_fingerprint( Ok(contact_id) }, ) - .await?; - Ok(contact_id) + .await? + { + Ok(Some(contact_id)) + } else if let Some(self_public_key) = load_self_public_key_opt(context).await? { + if self_public_key.dc_fingerprint().hex() == fingerprint { + Ok(Some(ContactId::SELF)) + } else { + Ok(None) + } + } else { + Ok(None) + } } /// Looks up PGP-contacts by email addresses. diff --git a/src/securejoin/securejoin_tests.rs b/src/securejoin/securejoin_tests.rs index 3f6cc72881..3bc0caf950 100644 --- a/src/securejoin/securejoin_tests.rs +++ b/src/securejoin/securejoin_tests.rs @@ -717,8 +717,7 @@ async fn test_lost_contact_confirm() { alice.recv_msg_trash(&sent).await; // Alice has Bob verified now. - let contact_bob = alice.add_or_lookup_pgp_contact(&bob) - .await; + let contact_bob = alice.add_or_lookup_pgp_contact(&bob).await; assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), true); // Alice sends vc-contact-confirm, but it gets lost. From 263fb2f32b22206a99fb48c405c0233745da3b72 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 15 Apr 2025 16:13:22 +0000 Subject: [PATCH 070/381] stock_str::stock_str_tests::test_stock_system_msg_add_member_by_me fix --- src/stock_str/stock_str_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stock_str/stock_str_tests.rs b/src/stock_str/stock_str_tests.rs index f64bb6fb6c..3fd678cf54 100644 --- a/src/stock_str/stock_str_tests.rs +++ b/src/stock_str/stock_str_tests.rs @@ -79,7 +79,7 @@ async fn test_stock_system_msg_add_member_by_me() { ); assert_eq!( msg_add_member_local(&t, alice_contact_id, ContactId::SELF).await, - "You added member alice@example.org." + "You added member Alice." ) } From 9ec62af9f0c00b4cd51926d94fe796a877b927e3 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 15 Apr 2025 16:51:12 +0000 Subject: [PATCH 071/381] cleanup --- src/qr.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/qr.rs b/src/qr.rs index 991a638a62..50f99acc28 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -11,7 +11,6 @@ use percent_encoding::{percent_decode_str, percent_encode, NON_ALPHANUMERIC}; use serde::Deserialize; pub(crate) use self::dclogin_scheme::configure_from_login_qr; -use crate::chat::ChatIdBlocked; use crate::config::Config; use crate::contact::{Contact, ContactId, Origin}; use crate::context::Context; From c7e4a6685259b7528389d362a1d870344d11a915 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 15 Apr 2025 16:51:12 +0000 Subject: [PATCH 072/381] create hidden 1:1 chat for gossip-verified contacts --- src/receive_imf.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index de92275c66..5bf39c25de 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -3056,12 +3056,18 @@ async fn mark_recipients_as_verified( if mimeparser.get_header(HeaderDef::ChatVerified).is_none() { return Ok(()); } - for id in to_ids.iter().filter_map(|&x| x) { - if id == ContactId::SELF { + for to_id in to_ids.iter().filter_map(|&x| x) { + if to_id == ContactId::SELF { continue; } - mark_contact_id_as_verified(context, id, from_id).await?; + mark_contact_id_as_verified(context, to_id, from_id).await?; + ChatId::set_protection_for_contact( + context, + to_id, + mimeparser.timestamp_sent, + ) + .await?; } Ok(()) From 5516710190e0c69677bffbe4764a9f76f3b16cda Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 15 Apr 2025 21:47:25 +0000 Subject: [PATCH 073/381] fix contact lookup --- src/contact.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/contact.rs b/src/contact.rs index bbfe65c5cc..45e81e62a1 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -890,8 +890,8 @@ impl Contact { .query_row( "SELECT id, name, addr, origin, authname FROM contacts - WHERE (?1<>'' AND fingerprint=?1) - OR (?1='' AND addr=?2 COLLATE NOCASE)", + WHERE fingerprint=?1 AND + (?1<>'' OR addr=?2 COLLATE NOCASE)", (fingerprint, addr), |row| { let row_id: u32 = row.get(0)?; From 3b49425a2a56f9bb14faad7730f9476693ab1ddb Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 15 Apr 2025 21:47:25 +0000 Subject: [PATCH 074/381] debug assertion --- src/test_utils.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test_utils.rs b/src/test_utils.rs index 6e2e46c158..deb0890b16 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -741,7 +741,9 @@ impl TestContext { /// Returns the [`Contact`] for the other [`TestContext`], creating it if necessary. pub async fn add_or_lookup_email_contact(&self, other: &TestContext) -> Contact { let contact_id = self.add_or_lookup_email_contact_id(other).await; - Contact::get_by_id(&self.ctx, contact_id).await.unwrap() + let contact = Contact::get_by_id(&self.ctx, contact_id).await.unwrap(); + debug_assert_eq!(contact.is_pgp_contact(), false); + contact } /// Returns the [`ContactId`] for the other [`TestContext`], creating it if necessary. From b2f6863e24f592b7140fa4010326446134f02ba1 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 15 Apr 2025 21:52:33 +0000 Subject: [PATCH 075/381] fix tests::verified_chats::test_create_verified_oneonone_chat --- src/tests/verified_chats.rs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/tests/verified_chats.rs b/src/tests/verified_chats.rs index b360c8cb68..fd168c135b 100644 --- a/src/tests/verified_chats.rs +++ b/src/tests/verified_chats.rs @@ -155,27 +155,19 @@ async fn test_create_verified_oneonone_chat() -> Result<()> { tcm.send_recv(&fiona_new, &alice, "I have a new device") .await; - // The chat should be and stay unprotected + // Alice gets a new unprotected chat with new Fiona contact. { - let chat = alice.get_chat(&fiona_new).await; + let chat = alice.get_pgp_chat(&fiona_new).await; assert!(!chat.is_protected()); - assert!(chat.is_protection_broken()); - - let msg1 = get_chat_msg(&alice, chat.id, 0, 3).await; - assert_eq!(msg1.get_info_type(), SystemMessage::ChatProtectionEnabled); - - let msg2 = get_chat_msg(&alice, chat.id, 1, 3).await; - assert_eq!(msg2.get_info_type(), SystemMessage::ChatProtectionDisabled); - let msg2 = get_chat_msg(&alice, chat.id, 2, 3).await; - assert_eq!(msg2.text, "I have a new device"); + let msg = get_chat_msg(&alice, chat.id, 0, 1).await; + assert_eq!(msg.text, "I have a new device"); // After recreating the chat, it should still be unprotected chat.id.delete(&alice).await?; let chat = alice.create_chat(&fiona_new).await; assert!(!chat.is_protected()); - assert!(!chat.is_protection_broken()); } Ok(()) From cf09d53c1a93a24ba285f2800e7d0b0d4658d07f Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 16 Apr 2025 16:52:30 +0000 Subject: [PATCH 076/381] api: add is_encrypted() to BasicChat and FullChat --- deltachat-jsonrpc/src/api/types/chat.rs | 5 +++++ src/chat.rs | 23 +++++++++++++++++++++++ src/mimefactory.rs | 23 +++-------------------- src/receive_imf.rs | 7 +------ 4 files changed, 32 insertions(+), 26 deletions(-) diff --git a/deltachat-jsonrpc/src/api/types/chat.rs b/deltachat-jsonrpc/src/api/types/chat.rs index b1ee3802ca..2d22b101a4 100644 --- a/deltachat-jsonrpc/src/api/types/chat.rs +++ b/deltachat-jsonrpc/src/api/types/chat.rs @@ -30,6 +30,7 @@ pub struct FullChat { /// in the contact profile /// if 1:1 chat with this contact exists and is protected. is_protected: bool, + is_encrypted: bool, profile_image: Option, //BLOBS ? archived: bool, pinned: bool, @@ -108,6 +109,7 @@ impl FullChat { id: chat_id, name: chat.name.clone(), is_protected: chat.is_protected(), + is_encrypted: chat.is_encrypted(context).await?, profile_image, //BLOBS ? archived: chat.get_visibility() == chat::ChatVisibility::Archived, pinned: chat.get_visibility() == chat::ChatVisibility::Pinned, @@ -159,6 +161,8 @@ pub struct BasicChat { /// in the contact profile /// if 1:1 chat with this contact exists and is protected. is_protected: bool, + + is_encrypted: bool, profile_image: Option, //BLOBS ? archived: bool, pinned: bool, @@ -187,6 +191,7 @@ impl BasicChat { id: chat_id, name: chat.name.clone(), is_protected: chat.is_protected(), + is_encrypted: chat.is_encrypted(context).await?, profile_image, //BLOBS ? archived: chat.get_visibility() == chat::ChatVisibility::Archived, pinned: chat.get_visibility() == chat::ChatVisibility::Pinned, diff --git a/src/chat.rs b/src/chat.rs index 29797802ed..2a88a754e4 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1925,6 +1925,29 @@ impl Chat { self.protected == ProtectionStatus::Protected } + /// Returns true if the chat is encrypted. + pub async fn is_encrypted(&self, context: &Context) -> Result { + let is_encrypted = self.is_protected() + || match self.typ { + Chattype::Single => { + let chat_contact_ids = get_chat_contacts(context, self.id).await?; + if let Some(contact_id) = chat_contact_ids.first() { + let contact = Contact::get_by_id(context, *contact_id).await?; + contact.is_pgp_contact() + } else { + true + } + } + Chattype::Group => { + // Do not encrypt ad-hoc groups. + !self.grpid.is_empty() + } + Chattype::Mailinglist => false, + Chattype::Broadcast => true, + }; + Ok(is_encrypted) + } + /// Returns true if the chat was protected, and then an incoming message broke this protection. /// /// This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag, diff --git a/src/mimefactory.rs b/src/mimefactory.rs index db1ecce1a9..ee9ef57a72 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -15,7 +15,7 @@ use tokio::fs; use crate::aheader::{Aheader, EncryptPreference}; use crate::blob::BlobObject; -use crate::chat::{self, get_chat_contacts, Chat}; +use crate::chat::{self, Chat}; use crate::config::Config; use crate::constants::ASM_SUBJECT; use crate::constants::{Chattype, DC_FROM_HANDSHAKE}; @@ -223,25 +223,8 @@ impl MimeFactory { { false } else { - chat.is_protected() - || msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default() - || match chat.typ { - Chattype::Single => { - let chat_contact_ids = get_chat_contacts(context, chat.id).await?; - if let Some(contact_id) = chat_contact_ids.first() { - let contact = Contact::get_by_id(context, *contact_id).await?; - contact.is_pgp_contact() - } else { - true - } - } - Chattype::Group => { - // Do not encrypt ad-hoc groups. - !chat.grpid.is_empty() - } - Chattype::Mailinglist => false, - Chattype::Broadcast => true, - } + msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default() + || chat.is_encrypted(context).await? }; let mut certificates = Vec::new(); diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 5bf39c25de..54a95ce3d6 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -3062,12 +3062,7 @@ async fn mark_recipients_as_verified( } mark_contact_id_as_verified(context, to_id, from_id).await?; - ChatId::set_protection_for_contact( - context, - to_id, - mimeparser.timestamp_sent, - ) - .await?; + ChatId::set_protection_for_contact(context, to_id, mimeparser.timestamp_sent).await?; } Ok(()) From 8b59dc4e01c83585b161721144646176e677f497 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 16 Apr 2025 17:40:23 +0000 Subject: [PATCH 077/381] api: add dc_chat_is_encrypted --- deltachat-ffi/deltachat.h | 15 +++++++++++++++ deltachat-ffi/src/lib.rs | 12 ++++++++++++ 2 files changed, 27 insertions(+) diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index adbf816683..7b2cfc9bbe 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -3837,6 +3837,21 @@ int dc_chat_can_send (const dc_chat_t* chat); int dc_chat_is_protected (const dc_chat_t* chat); +/** + * Check if the chat is encrypted. + * + * 1:1 chats with PGP-contacts and group chats with PGP-contacts + * are encrypted. + * 1:1 chats with emails contacts and ad-hoc groups + * created for email threads are not encrypted. + * + * @memberof dc_chat_t + * @param chat The chat object. + * @return 1=chat is encrypted, 0=chat is not encrypted. + */ +int dc_chat_is_encrypted (const dc_chat_t *chat); + + /** * Checks if the chat was protected, and then an incoming message broke this protection. * diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 5ad6f92867..269629fa3b 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -3195,6 +3195,18 @@ pub unsafe extern "C" fn dc_chat_is_protected(chat: *mut dc_chat_t) -> libc::c_i ffi_chat.chat.is_protected() as libc::c_int } +#[no_mangle] +pub unsafe extern "C" fn dc_chat_is_encrypted(chat: *mut dc_chat_t) -> libc::c_int { + if chat.is_null() { + eprintln!("ignoring careless call to dc_chat_is_encrypted()"); + return 0; + } + let ffi_chat = &*chat; + + block_on(ffi_chat.chat.is_encrypted(&ffi_chat.context)) + .unwrap_or_log_default(&ffi_chat.context, "Failed dc_chat_is_encrypted") as libc::c_int +} + #[no_mangle] pub unsafe extern "C" fn dc_chat_is_protection_broken(chat: *mut dc_chat_t) -> libc::c_int { if chat.is_null() { From 6f86a3477183bd01e92edc0a0d9c378d3d64930e Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 16 Apr 2025 18:40:16 +0000 Subject: [PATCH 078/381] fix test_oneone_gossip --- src/receive_imf.rs | 64 +++++++++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 54a95ce3d6..5579739ebc 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -330,21 +330,22 @@ pub(crate) async fn receive_imf_inner( let to_ids: Vec>; let past_ids: Vec>; - if mime_parser.get_chat_group_id().is_some() { - to_ids = add_or_lookup_pgp_contacts_by_address_list( - context, - &mime_parser.recipients, - &mime_parser.gossiped_keys, - if !mime_parser.incoming { - Origin::OutgoingTo - } else if incoming_origin.is_known() { - Origin::IncomingTo - } else { - Origin::IncomingUnknownTo - }, - ) - .await?; + let pgp_to_ids = add_or_lookup_pgp_contacts_by_address_list( + context, + &mime_parser.recipients, + &mime_parser.gossiped_keys, + if !mime_parser.incoming { + Origin::OutgoingTo + } else if incoming_origin.is_known() { + Origin::IncomingTo + } else { + Origin::IncomingUnknownTo + }, + ) + .await?; + if mime_parser.get_chat_group_id().is_some() { + to_ids = pgp_to_ids; if let Some(chat_id) = chat_id { past_ids = lookup_pgp_contacts_by_address_list(context, &mime_parser.past_members, chat_id) @@ -354,18 +355,29 @@ pub(crate) async fn receive_imf_inner( past_ids = vec![None; mime_parser.past_members.len()]; } } else { - to_ids = add_or_lookup_contacts_by_address_list( - context, - &mime_parser.recipients, - if !mime_parser.incoming { - Origin::OutgoingTo - } else if incoming_origin.is_known() { - Origin::IncomingTo - } else { - Origin::IncomingUnknownTo - }, - ) - .await?; + if pgp_to_ids.len() == 1 + && pgp_to_ids + .get(0) + .is_some_and(|contact_id| contact_id.is_some()) + { + // There is a single recipient and we have + // mapped it to a PGP contact. + // This is a 1:1 PGP-chat. + to_ids = pgp_to_ids + } else { + to_ids = add_or_lookup_contacts_by_address_list( + context, + &mime_parser.recipients, + if !mime_parser.incoming { + Origin::OutgoingTo + } else if incoming_origin.is_known() { + Origin::IncomingTo + } else { + Origin::IncomingUnknownTo + }, + ) + .await?; + } past_ids = add_or_lookup_contacts_by_address_list( context, From 1b5a136c203c786177aa979454bdefbb467b9d00 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 16 Apr 2025 18:59:12 +0000 Subject: [PATCH 079/381] assertion message --- src/test_utils.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/test_utils.rs b/src/test_utils.rs index deb0890b16..e0404e336c 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -1344,7 +1344,13 @@ pub(crate) async fn get_chat_msg( asserted_msgs_count: usize, ) -> Message { let msgs = chat::get_chat_msgs(&t.ctx, chat_id).await.unwrap(); - assert_eq!(msgs.len(), asserted_msgs_count); + assert_eq!( + msgs.len(), + asserted_msgs_count, + "expected {} messages in a chat but {} found", + asserted_msgs_count, + msgs.len() + ); let msg_id = if let ChatItem::Message { msg_id } = msgs[index] { msg_id } else { From 7db4a3ebbc5f8c83e0bbc2f528658d65c0a5d8b3 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 16 Apr 2025 19:05:32 +0000 Subject: [PATCH 080/381] fixed tests::verified_chats::test_degrade_verified_oneonone_chat --- src/tests/verified_chats.rs | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/tests/verified_chats.rs b/src/tests/verified_chats.rs index fd168c135b..31970efab8 100644 --- a/src/tests/verified_chats.rs +++ b/src/tests/verified_chats.rs @@ -232,6 +232,10 @@ async fn test_create_unverified_oneonone_chat() -> Result<()> { Ok(()) } +/// Tests that receiving unencrypted message +/// does not disable protection of 1:1 chat. +/// +/// Instead, an email-chat is created. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_degrade_verified_oneonone_chat() -> Result<()> { let mut tcm = TestContextManager::new(); @@ -255,23 +259,16 @@ async fn test_degrade_verified_oneonone_chat() -> Result<()> { ) .await?; - let contact_id = Contact::lookup_id_by_addr(&alice, "bob@example.net", Origin::Hidden) - .await? - .unwrap(); - - let msg0 = get_chat_msg(&alice, alice_chat.id, 0, 3).await; + let msg0 = get_chat_msg(&alice, alice_chat.id, 0, 1).await; let enabled = stock_str::chat_protection_enabled(&alice).await; assert_eq!(msg0.text, enabled); assert_eq!(msg0.param.get_cmd(), SystemMessage::ChatProtectionEnabled); - let msg1 = get_chat_msg(&alice, alice_chat.id, 1, 3).await; - let disabled = stock_str::chat_protection_disabled(&alice, contact_id).await; - assert_eq!(msg1.text, disabled); - assert_eq!(msg1.param.get_cmd(), SystemMessage::ChatProtectionDisabled); - - let msg2 = get_chat_msg(&alice, alice_chat.id, 2, 3).await; - assert_eq!(msg2.text, "hello".to_string()); - assert!(!msg2.is_system_message()); + let email_chat = alice.get_chat(&bob).await; + assert!(!email_chat.is_encrypted(&alice).await?); + let email_msg = get_chat_msg(&alice, email_chat.id, 0, 1).await; + assert_eq!(email_msg.text, "hello".to_string()); + assert!(!email_msg.is_system_message()); Ok(()) } From eebd1042e5504f6bcacb0b17fe4048efed50d593 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 16 Apr 2025 19:56:40 +0000 Subject: [PATCH 081/381] remove test_create_oneonone_chat_with_former_verified_contact contact cannot become unverified --- src/tests/verified_chats.rs | 37 ------------------------------------- 1 file changed, 37 deletions(-) diff --git a/src/tests/verified_chats.rs b/src/tests/verified_chats.rs index 31970efab8..f1c10ba46a 100644 --- a/src/tests/verified_chats.rs +++ b/src/tests/verified_chats.rs @@ -798,43 +798,6 @@ async fn test_verify_then_verify_again() -> Result<()> { Ok(()) } -/// Regression test: -/// - Verify a contact -/// - The contact stops using DC and sends a message from a classical MUA instead -/// - Delete the 1:1 chat -/// - Create a 1:1 chat -/// - Check that the created chat is not marked as protected -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_create_oneonone_chat_with_former_verified_contact() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - enable_verified_oneonone_chats(&[&alice]).await; - - mark_as_verified(&alice, &bob).await; - - receive_imf( - &alice, - b"Subject: Message from bob\r\n\ - From: \r\n\ - To: \r\n\ - Date: Mon, 12 Dec 2022 14:33:39 +0000\r\n\ - Message-ID: \r\n\ - \r\n\ - Heyho!\r\n", - false, - ) - .await - .unwrap() - .unwrap(); - - alice.create_chat(&bob).await; - - assert_verified(&alice, &bob, ProtectionStatus::Unprotected).await; - - Ok(()) -} - /// Tests that on the second device of a protected group creator the first message is /// `SystemMessage::ChatProtectionEnabled` and the second one is the message populating the group. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] From 39f2c3e4e2e6f47ffbbcbe16887fcbd22ebe2d3c Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 16 Apr 2025 20:35:13 +0000 Subject: [PATCH 082/381] fix chat::chat_tests::test_sync_blocked --- src/chat/chat_tests.rs | 15 ++++++++------- src/contact.rs | 8 +++++++- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 59d0e5dc96..54a7e3f3a8 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -2959,12 +2959,13 @@ async fn test_blob_renaming() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_sync_blocked() -> Result<()> { - let alice0 = &TestContext::new_alice().await; - let alice1 = &TestContext::new_alice().await; + let mut tcm = TestContextManager::new(); + let alice0 = &tcm.alice().await; + let alice1 = &tcm.alice().await; for a in [alice0, alice1] { a.set_config_bool(Config::SyncMsgs, true).await?; } - let bob = TestContext::new_bob().await; + let bob = &tcm.bob().await; let ba_chat = bob.create_chat(alice0).await; let sent_msg = bob.send_text(ba_chat.id, "hi").await; @@ -2972,16 +2973,16 @@ async fn test_sync_blocked() -> Result<()> { alice1.recv_msg(&sent_msg).await; let a0b_contact_id = alice0.add_or_lookup_contact_id(&bob).await; - assert_eq!(alice1.get_chat(&bob).await.blocked, Blocked::Request); + assert_eq!(alice1.get_pgp_chat(&bob).await.blocked, Blocked::Request); a0b_chat_id.accept(alice0).await?; sync(alice0, alice1).await; - assert_eq!(alice1.get_chat(&bob).await.blocked, Blocked::Not); + assert_eq!(alice1.get_pgp_chat(&bob).await.blocked, Blocked::Not); a0b_chat_id.block(alice0).await?; sync(alice0, alice1).await; - assert_eq!(alice1.get_chat(&bob).await.blocked, Blocked::Yes); + assert_eq!(alice1.get_pgp_chat(&bob).await.blocked, Blocked::Yes); a0b_chat_id.unblock(alice0).await?; sync(alice0, alice1).await; - assert_eq!(alice1.get_chat(&bob).await.blocked, Blocked::Not); + assert_eq!(alice1.get_pgp_chat(&bob).await.blocked, Blocked::Not); // Unblocking a 1:1 chat doesn't unblock the contact currently. Contact::unblock(alice0, a0b_contact_id).await?; diff --git a/src/contact.rs b/src/contact.rs index 45e81e62a1..064ceff4f1 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -1725,9 +1725,15 @@ WHERE type=? AND id IN ( true => chat::SyncAction::Block, false => chat::SyncAction::Unblock, }; + let sync_id = if let Some(fingerprint) = contact.fingerprint() { + chat::SyncId::ContactFingerprint(fingerprint.to_string()) + } else { + chat::SyncId::ContactAddr(contact.addr.clone()) + }; + chat::sync( context, - chat::SyncId::ContactAddr(contact.addr.clone()), + sync_id, action, ) .await From 2c131c4b1e46cd6269935057402dc161d1a3d47c Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 16 Apr 2025 20:41:30 +0000 Subject: [PATCH 083/381] fix chat::chat_tests::test_sync_accept_before_first_msg --- src/chat/chat_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 54a7e3f3a8..51b3a7fe10 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -3046,7 +3046,7 @@ async fn test_sync_accept_before_first_msg() -> Result<()> { a0b_chat_id.accept(alice0).await?; let a0b_contact = Contact::get_by_id(alice0, a0b_contact_id).await?; assert_eq!(a0b_contact.origin, Origin::CreateChat); - assert_eq!(alice0.get_chat(bob).await.blocked, Blocked::Not); + assert_eq!(alice0.get_pgp_chat(bob).await.blocked, Blocked::Not); sync(alice0, alice1).await; let alice1_contacts = Contact::get_all(alice1, 0, None).await?; From 6a719495c44035cb57e319e80aba3741b5ac3d61 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 16 Apr 2025 20:46:08 +0000 Subject: [PATCH 084/381] fix test_decode_openpgp_invalid_token --- src/qr/qr_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qr/qr_tests.rs b/src/qr/qr_tests.rs index 7b7aae0fe7..ca96f6b3dd 100644 --- a/src/qr/qr_tests.rs +++ b/src/qr/qr_tests.rs @@ -287,7 +287,7 @@ async fn test_decode_openpgp_group() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_decode_openpgp_invalid_token() -> Result<()> { - let ctx = TestContext::new().await; + let ctx = TestContext::new_alice().await; // Token cannot contain "/" let qr = check_qr( From 570e88bfb333752c8f475a413cb992116b34fae6 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 16 Apr 2025 21:06:19 +0000 Subject: [PATCH 085/381] do not send unencrypted message in test_change_primary_self_addr --- src/tests/aeap.rs | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/src/tests/aeap.rs b/src/tests/aeap.rs index c1cfe1c06d..140f653c22 100644 --- a/src/tests/aeap.rs +++ b/src/tests/aeap.rs @@ -34,30 +34,6 @@ async fn test_change_primary_self_addr() -> Result<()> { let alice_bob_chat = alice.create_chat(&bob).await; assert_eq!(alice_msg.chat_id, alice_bob_chat.id); - tcm.section("Bob sends a message to Alice without In-Reply-To"); - // Even if Bob sends a message to Alice without In-Reply-To, - // it's still assigned to the 1:1 chat with Bob and not to - // a group (without secondary addresses, an ad-hoc group - // would be created) - receive_imf( - &alice, - b"From: bob@example.net -To: alice@example.org -Chat-Version: 1.0 -Message-ID: <456@example.com> - -Message w/out In-Reply-To -", - false, - ) - .await?; - - let alice_msg = alice.get_last_msg().await; - - assert_eq!(alice_msg.text, "Message w/out In-Reply-To"); - assert_eq!(alice_msg.get_showpadlock(), false); - assert_eq!(alice_msg.chat_id, alice_bob_chat.id); - Ok(()) } From ac8c61755d9b986c94eecdad82bb615328184d63 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 16 Apr 2025 21:09:23 +0000 Subject: [PATCH 086/381] remove webxdc::webxdc_tests::test_webxdc_opportunistic_encryption there is no opportunistic encryption --- src/webxdc/webxdc_tests.rs | 52 -------------------------------------- 1 file changed, 52 deletions(-) diff --git a/src/webxdc/webxdc_tests.rs b/src/webxdc/webxdc_tests.rs index c0a9d347c1..76646d3777 100644 --- a/src/webxdc/webxdc_tests.rs +++ b/src/webxdc/webxdc_tests.rs @@ -1584,58 +1584,6 @@ async fn test_webxdc_info_msg_no_cleanup_on_interrupted_series() -> Result<()> { Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_webxdc_opportunistic_encryption() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - - // Bob sends sth. to Alice, Alice has Bob's key - let bob_chat_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "chat").await?; - add_contact_to_chat( - &bob, - bob_chat_id, - Contact::create(&bob, "", "alice@example.org").await?, - ) - .await?; - send_text_msg(&bob, bob_chat_id, "populate".to_string()).await?; - alice.recv_msg(&bob.pop_sent_msg().await).await; - - // Alice sends instance+update to Bob - let alice_chat_id = alice.get_last_msg().await.chat_id; - alice_chat_id.accept(&alice).await?; - let alice_instance = send_webxdc_instance(&alice, alice_chat_id).await?; - let sent1 = &alice.pop_sent_msg().await; - alice - .send_webxdc_status_update(alice_instance.id, r#"{"payload":42}"#) - .await?; - alice.flush_status_updates().await?; - let sent2 = &alice.pop_sent_msg().await; - let update_msg = sent2.load_from_db().await; - assert!(alice_instance.get_showpadlock()); - assert!(update_msg.get_showpadlock()); - - // Bob receives instance+update - let bob_instance = bob.recv_msg(sent1).await; - bob.recv_msg_trash(sent2).await; - assert!(bob_instance.get_showpadlock()); - - // Bob adds Claire with unknown key, update to Alice+Claire cannot be encrypted - add_contact_to_chat( - &bob, - bob_chat_id, - Contact::create(&bob, "", "claire@example.org").await?, - ) - .await?; - bob.send_webxdc_status_update(bob_instance.id, r#"{"payload":43}"#) - .await?; - bob.flush_status_updates().await?; - let sent3 = bob.pop_sent_msg().await; - let update_msg = sent3.load_from_db().await; - assert!(!update_msg.get_showpadlock()); - - Ok(()) -} - // check that `info.internet_access` is not set for normal, non-integrated webxdc - // even if they use the deprecated option `request_internet_access` in manifest.toml #[tokio::test(flavor = "multi_thread", worker_threads = 2)] From 244163599c6ff853a1b136b53fc29279e8b39455 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 17 Apr 2025 00:21:24 +0000 Subject: [PATCH 087/381] execute_migration_transaction --- src/contact.rs | 12 ++++------ src/sql/migrations.rs | 55 +++++++++++++++++++++++-------------------- 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/src/contact.rs b/src/contact.rs index 064ceff4f1..25c61ff400 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -1731,14 +1731,10 @@ WHERE type=? AND id IN ( chat::SyncId::ContactAddr(contact.addr.clone()) }; - chat::sync( - context, - sync_id, - action, - ) - .await - .log_err(context) - .ok(); + chat::sync(context, sync_id, action) + .await + .log_err(context) + .ok(); } } diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 98e5b39b71..286c730601 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1224,32 +1224,37 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint); inc_and_check(&mut migration_version, 132)?; if dbversion < migration_version { - sql.execute_migration( - "ALTER TABLE contacts ADD COLUMN fingerprint TEXT NOT NULL DEFAULT ''; - - -- Verifier is an ID of the verifier contact. - -- 0 if the contact is not verified. - ALTER TABLE contacts ADD COLUMN verifier INTEGER NOT NULL DEFAULT 0; - - CREATE INDEX contacts_fingerprint_index ON contacts (fingerprint); + sql.execute_migration_transaction( + |transaction| { + transaction.execute_batch( + "ALTER TABLE contacts ADD COLUMN fingerprint TEXT NOT NULL DEFAULT ''; + + -- Verifier is an ID of the verifier contact. + -- 0 if the contact is not verified. + ALTER TABLE contacts ADD COLUMN verifier INTEGER NOT NULL DEFAULT 0; + + CREATE INDEX contacts_fingerprint_index ON contacts (fingerprint); + + CREATE TABLE public_keys ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fingerprint TEXT NOT NULL UNIQUE, -- Upper-case fingerprint of the key. + public_key BLOB NOT NULL -- Binary key, not ASCII-armored + ) STRICT; + CREATE INDEX public_key_index ON public_keys (fingerprint); + + INSERT INTO public_keys (fingerprint, public_key) + SELECT public_key_fingerprint, public_key FROM acpeerstates; + INSERT OR IGNORE INTO public_keys (fingerprint, public_key) + SELECT gossip_key_fingerprint, gossip_key FROM acpeerstates; + INSERT OR IGNORE INTO public_keys (fingerprint, public_key) + SELECT verified_key_fingerprint, verified_key FROM acpeerstates; + INSERT OR IGNORE INTO public_keys (fingerprint, public_key) + SELECT secondary_verified_key_fingerprint, secondary_verified_key FROM acpeerstates; + ")?; - CREATE TABLE public_keys ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - fingerprint TEXT NOT NULL UNIQUE, -- Upper-case fingerprint of the key. - public_key BLOB NOT NULL -- Binary key, not ASCII-armored - ) STRICT; - CREATE INDEX public_key_index ON public_keys (fingerprint); - - INSERT INTO public_keys (fingerprint, public_key) - SELECT public_key_fingerprint, public_key FROM acpeerstates; - INSERT OR IGNORE INTO public_keys (fingerprint, public_key) - SELECT gossip_key_fingerprint, gossip_key FROM acpeerstates; - INSERT OR IGNORE INTO public_keys (fingerprint, public_key) - SELECT verified_key_fingerprint, verified_key FROM acpeerstates; - INSERT OR IGNORE INTO public_keys (fingerprint, public_key) - SELECT secondary_verified_key_fingerprint, secondary_verified_key FROM acpeerstates; - ", - migration_version, + Ok(()) + }, + migration_version ) .await?; } From e64e0ff8973061fad58135540e095e1b77fc8fd2 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 17 Apr 2025 00:22:10 +0000 Subject: [PATCH 088/381] cleanup --- src/tests/verified_chats.rs | 2 +- src/webxdc/webxdc_tests.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/tests/verified_chats.rs b/src/tests/verified_chats.rs index f1c10ba46a..d3a7319793 100644 --- a/src/tests/verified_chats.rs +++ b/src/tests/verified_chats.rs @@ -7,7 +7,7 @@ use crate::chat::{ use crate::chatlist::Chatlist; use crate::config::Config; use crate::constants::{Chattype, DC_GCL_FOR_FORWARDING}; -use crate::contact::{Contact, ContactId, Origin}; +use crate::contact::{Contact, ContactId}; use crate::message::Message; use crate::mimefactory::MimeFactory; use crate::mimeparser::SystemMessage; diff --git a/src/webxdc/webxdc_tests.rs b/src/webxdc/webxdc_tests.rs index 76646d3777..214dd6fff6 100644 --- a/src/webxdc/webxdc_tests.rs +++ b/src/webxdc/webxdc_tests.rs @@ -10,7 +10,6 @@ use crate::chat::{ }; use crate::chatlist::Chatlist; use crate::config::Config; -use crate::contact::Contact; use crate::download::DownloadState; use crate::ephemeral; use crate::receive_imf::{receive_imf, receive_imf_from_inbox}; From bd0802a2b560d8f1ef7145a0a61e6ab16777fecb Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 17 Apr 2025 01:17:14 +0000 Subject: [PATCH 089/381] pgp contact in AEAP test --- src/tests/aeap.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/tests/aeap.rs b/src/tests/aeap.rs index 140f653c22..278a49b290 100644 --- a/src/tests/aeap.rs +++ b/src/tests/aeap.rs @@ -139,9 +139,7 @@ async fn check_aeap_transition( ); } - let old_contact = Contact::create(&bob, "Alice", "alice@example.org") - .await - .unwrap(); + let old_contact = bob.add_or_lookup_contact_id(&alice).await; for group in &groups { chat::add_contact_to_chat(&bob, *group, old_contact) .await From c55384443387b8707c38ec9ea6a40d3c0cdec27f Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 17 Apr 2025 19:26:10 +0000 Subject: [PATCH 090/381] aeap test fixes --- src/tests/aeap.rs | 161 +++++++++------------------------------------- 1 file changed, 31 insertions(+), 130 deletions(-) diff --git a/src/tests/aeap.rs b/src/tests/aeap.rs index 278a49b290..a7253b3803 100644 --- a/src/tests/aeap.rs +++ b/src/tests/aeap.rs @@ -46,76 +46,44 @@ use ChatForTransition::*; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_aeap_transition_0() { - check_aeap_transition(OneToOne, false, false).await; + check_aeap_transition(OneToOne, false).await; } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_aeap_transition_1() { - check_aeap_transition(GroupChat, false, false).await; + check_aeap_transition(GroupChat, false).await; } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_aeap_transition_0_verified() { - check_aeap_transition(OneToOne, true, false).await; + check_aeap_transition(OneToOne, true).await; } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_aeap_transition_1_verified() { - check_aeap_transition(GroupChat, true, false).await; + check_aeap_transition(GroupChat, true).await; } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_aeap_transition_2_verified() { - check_aeap_transition(VerifiedGroup, true, false).await; + check_aeap_transition(VerifiedGroup, true).await; } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_aeap_transition_0_bob_knew_new_addr() { - check_aeap_transition(OneToOne, false, true).await; -} -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_aeap_transition_1_bob_knew_new_addr() { - check_aeap_transition(GroupChat, false, true).await; -} -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_aeap_transition_0_verified_bob_knew_new_addr() { - check_aeap_transition(OneToOne, true, true).await; -} -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_aeap_transition_1_verified_bob_knew_new_addr() { - check_aeap_transition(GroupChat, true, true).await; -} -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_aeap_transition_2_verified_bob_knew_new_addr() { - check_aeap_transition(VerifiedGroup, true, true).await; -} - -/// Happy path test for AEAP in various configurations. +/// Happy path test for AEAP. /// - `chat_for_transition`: Which chat the transition message should be sent in /// - `verified`: Whether Alice and Bob verified each other -/// - `bob_knew_new_addr`: Whether Bob already had a chat with Alice's new address async fn check_aeap_transition( chat_for_transition: ChatForTransition, verified: bool, - bob_knew_new_addr: bool, ) { - // Alice's new address is "fiona@example.net" so that we can test - // the case where Bob already had contact with Alice's new address - const ALICE_NEW_ADDR: &str = "fiona@example.net"; + const ALICE_NEW_ADDR: &str = "alice2@example.net"; let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - - if bob_knew_new_addr { - let fiona = tcm.fiona().await; + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; - tcm.send_recv_accept(&fiona, &bob, "Hi").await; - tcm.send_recv(&bob, &fiona, "Hi back").await; - } - - tcm.send_recv_accept(&alice, &bob, "Hi").await; - tcm.send_recv(&bob, &alice, "Hi back").await; + tcm.send_recv_accept(alice, bob, "Hi").await; + tcm.send_recv(bob, alice, "Hi back").await; if verified { - mark_as_verified(&alice, &bob).await; - mark_as_verified(&bob, &alice).await; + mark_as_verified(alice, bob).await; + mark_as_verified(bob, alice).await; } let mut groups = vec![ @@ -139,20 +107,9 @@ async fn check_aeap_transition( ); } - let old_contact = bob.add_or_lookup_contact_id(&alice).await; + let alice_contact = bob.add_or_lookup_contact_id(&alice).await; for group in &groups { - chat::add_contact_to_chat(&bob, *group, old_contact) - .await - .unwrap(); - } - - // Already add the new contact to one of the groups. - // We can then later check that the contact isn't in the group twice. - let already_new_contact = Contact::create(&bob, "Alice", ALICE_NEW_ADDR) - .await - .unwrap(); - if verified { - chat::add_contact_to_chat(&bob, groups[2], already_new_contact) + chat::add_contact_to_chat(&bob, *group, alice_contact) .await .unwrap(); } @@ -188,15 +145,12 @@ async fn check_aeap_transition( tcm.section("Check that the AEAP transition worked"); check_that_transition_worked( - &groups[2..], - &alice, - "alice@example.org", + bob, + &groups, + alice_contact, ALICE_NEW_ADDR, - "Alice", - &bob, ) .await; - check_no_transition_done(&groups[0..2], "alice@example.org", &bob).await; tcm.section("Test switching back"); tcm.change_addr(&alice, "alice@example.org").await; @@ -207,95 +161,42 @@ async fn check_aeap_transition( assert_eq!(recvd.text, "Hello from my old addr!"); check_that_transition_worked( - &groups[2..], - &alice, - // Note that "alice@example.org" and ALICE_NEW_ADDR are switched now: - ALICE_NEW_ADDR, + bob, + &groups, + alice_contact, "alice@example.org", - "Alice", - &bob, ) .await; } async fn check_that_transition_worked( - groups: &[ChatId], - alice: &TestContext, - old_alice_addr: &str, - new_alice_addr: &str, - name: &str, bob: &TestContext, + groups: &[ChatId], + alice_contact_id: ContactId, + alice_addr: &str, ) { - let new_contact = Contact::lookup_id_by_addr(bob, new_alice_addr, contact::Origin::Unknown) - .await - .unwrap() - .unwrap(); - for group in groups { let members = chat::get_chat_contacts(bob, *group).await.unwrap(); - // In all the groups, exactly Bob and Alice's new number are members. - // (and Alice's new number isn't in there twice) + // In all the groups, exactly Bob and Alice are members. assert_eq!( members.len(), 2, "Group {} has members {:?}, but should have members {:?} and {:?}", group, &members, - new_contact, + alice_contact_id, ContactId::SELF ); assert!( - members.contains(&new_contact), - "Group {group} lacks {new_contact}" + members.contains(&alice_contact_id), + "Group {group} lacks {alice_contact_id}" ); assert!(members.contains(&ContactId::SELF)); - - let info_msg = get_last_info_msg(bob, *group).await.unwrap(); - let expected_text = - stock_str::aeap_addr_changed(bob, name, old_alice_addr, new_alice_addr).await; - assert_eq!(info_msg.text, expected_text); - assert_eq!(info_msg.from_id, ContactId::INFO); - - let msg = format!("Sending to group {group}"); - let sent = bob.send_text(*group, &msg).await; - let recvd = alice.recv_msg(&sent).await; - assert_eq!(recvd.text, msg); } -} -async fn check_no_transition_done(groups: &[ChatId], old_alice_addr: &str, bob: &TestContext) { - let old_contact = Contact::lookup_id_by_addr(bob, old_alice_addr, contact::Origin::Unknown) - .await - .unwrap() - .unwrap(); - - for group in groups { - let members = chat::get_chat_contacts(bob, *group).await.unwrap(); - // In all the groups, exactly Bob and Alice's _old_ number are members. - assert_eq!( - members.len(), - 2, - "Group {} has members {:?}, but should have members {:?} and {:?}", - group, - &members, - old_contact, - ContactId::SELF - ); - assert!(members.contains(&old_contact)); - assert!(members.contains(&ContactId::SELF)); - - let last_info_msg = get_last_info_msg(bob, *group).await; - assert!( - last_info_msg.is_none(), - "{last_info_msg:?} shouldn't be there (or it's an unrelated info msg)" - ); - - let sent = bob.send_text(*group, "hi").await; - let msg = Message::load_from_db(bob, sent.sender_msg_id) - .await - .unwrap(); - assert_eq!(msg.get_showpadlock(), true); - } + // Test that the email address of Alice is updated. + let alice_contact = Contact::get_by_id(bob, alice_contact_id).await.unwrap(); + assert_eq!(alice_contact.get_addr(), alice_addr); } async fn get_last_info_msg(t: &TestContext, chat_id: ChatId) -> Option { From 86a8e048dc8ee387f323d3ba0572b0e9e02bdfd4 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 18 Apr 2025 03:15:38 +0000 Subject: [PATCH 091/381] update contact address on AEAP --- src/receive_imf.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 5579739ebc..b9d3074912 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -709,6 +709,15 @@ pub async fn from_field_to_contact_id( let contact = Contact::get_by_id(context, from_id).await?; let from_id_blocked = contact.blocked; let incoming_origin = contact.origin; + + context + .sql + .execute( + "UPDATE contacts SET addr=? WHERE id=?", + (from_addr, from_id), + ) + .await?; + Ok(Some((from_id, from_id_blocked, incoming_origin))) } } From 18bf0cde410239b4e9166e299f356a8fed4af552 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 18 Apr 2025 03:15:38 +0000 Subject: [PATCH 092/381] cleanup --- src/tests/aeap.rs | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/src/tests/aeap.rs b/src/tests/aeap.rs index a7253b3803..60e85b483a 100644 --- a/src/tests/aeap.rs +++ b/src/tests/aeap.rs @@ -68,10 +68,7 @@ async fn test_aeap_transition_2_verified() { /// Happy path test for AEAP. /// - `chat_for_transition`: Which chat the transition message should be sent in /// - `verified`: Whether Alice and Bob verified each other -async fn check_aeap_transition( - chat_for_transition: ChatForTransition, - verified: bool, -) { +async fn check_aeap_transition(chat_for_transition: ChatForTransition, verified: bool) { const ALICE_NEW_ADDR: &str = "alice2@example.net"; let mut tcm = TestContextManager::new(); @@ -144,13 +141,7 @@ async fn check_aeap_transition( assert_eq!(recvd.text, "Hello from my new addr!"); tcm.section("Check that the AEAP transition worked"); - check_that_transition_worked( - bob, - &groups, - alice_contact, - ALICE_NEW_ADDR, - ) - .await; + check_that_transition_worked(bob, &groups, alice_contact, ALICE_NEW_ADDR).await; tcm.section("Test switching back"); tcm.change_addr(&alice, "alice@example.org").await; @@ -160,13 +151,7 @@ async fn check_aeap_transition( let recvd = bob.recv_msg(&sent).await; assert_eq!(recvd.text, "Hello from my old addr!"); - check_that_transition_worked( - bob, - &groups, - alice_contact, - "alice@example.org", - ) - .await; + check_that_transition_worked(bob, &groups, alice_contact, "alice@example.org").await; } async fn check_that_transition_worked( From e59704f45a9fd6ca8e0893264fadeac8a5ef39b5 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 19 Apr 2025 00:57:50 +0000 Subject: [PATCH 093/381] cleanup --- src/securejoin.rs | 19 ------------------- src/tests/aeap.rs | 1 - 2 files changed, 20 deletions(-) diff --git a/src/securejoin.rs b/src/securejoin.rs index 1751e341b7..5a038f5ab0 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -585,25 +585,6 @@ async fn secure_connection_established( Ok(()) } -async fn could_not_establish_secure_connection( - context: &Context, - contact_id: ContactId, - chat_id: ChatId, - details: &str, -) -> Result<()> { - let contact = Contact::get_by_id(context, contact_id).await?; - let mut msg = stock_str::contact_not_verified(context, &contact).await; - msg += " ("; - msg += details; - msg += ")"; - chat::add_info_msg(context, chat_id, &msg, time()).await?; - warn!( - context, - "StockMessage::ContactNotVerified posted to 1:1 chat ({})", details - ); - Ok(()) -} - /* ****************************************************************************** * Tools: Misc. ******************************************************************************/ diff --git a/src/tests/aeap.rs b/src/tests/aeap.rs index 60e85b483a..7067a86805 100644 --- a/src/tests/aeap.rs +++ b/src/tests/aeap.rs @@ -7,7 +7,6 @@ use crate::contact::ContactId; use crate::message::Message; use crate::receive_imf::receive_imf; use crate::securejoin::get_securejoin_qr; -use crate::stock_str; use crate::test_utils::mark_as_verified; use crate::test_utils::TestContext; use crate::test_utils::TestContextManager; From 0d0b57209b769662cf024ae3d6e1f1b1a5809b0f Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 19 Apr 2025 23:20:29 +0000 Subject: [PATCH 094/381] fix test_outgoing_mua_msg --- src/tests/verified_chats.rs | 5 +++++ test-data/golden/test_outgoing_mua_msg | 5 +---- test-data/golden/test_outgoing_mua_msg_pgp | 6 ++++++ 3 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 test-data/golden/test_outgoing_mua_msg_pgp diff --git a/src/tests/verified_chats.rs b/src/tests/verified_chats.rs index d3a7319793..7a129908f3 100644 --- a/src/tests/verified_chats.rs +++ b/src/tests/verified_chats.rs @@ -589,9 +589,14 @@ async fn test_outgoing_mua_msg() -> Result<()> { .unwrap(); tcm.send_recv(&alice, &bob, "Sending with DC again").await; + // Unencrypted message from MUA gets into a separate chat. + // PGP chat gets all encrypted messages. alice .golden_test_chat(sent.chat_id, "test_outgoing_mua_msg") .await; + alice + .golden_test_chat(alice.get_pgp_chat(&bob).await.id, "test_outgoing_mua_msg_pgp") + .await; Ok(()) } diff --git a/test-data/golden/test_outgoing_mua_msg b/test-data/golden/test_outgoing_mua_msg index aa2bac6201..abab166cbe 100644 --- a/test-data/golden/test_outgoing_mua_msg +++ b/test-data/golden/test_outgoing_mua_msg @@ -1,7 +1,4 @@ -Single#Chat#10: bob@example.net [PGP bob@example.net] 🛡️ +Single#Chat#11: bob@example.net [bob@example.net] -------------------------------------------------------------------------------- -Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️] -Msg#11🔒: (Contact#Contact#10): Heyho from DC [FRESH] Msg#12: Me (Contact#Contact#Self): One classical MUA message √ -Msg#13🔒: Me (Contact#Contact#Self): Sending with DC again √ -------------------------------------------------------------------------------- diff --git a/test-data/golden/test_outgoing_mua_msg_pgp b/test-data/golden/test_outgoing_mua_msg_pgp new file mode 100644 index 0000000000..dca2e58e5a --- /dev/null +++ b/test-data/golden/test_outgoing_mua_msg_pgp @@ -0,0 +1,6 @@ +Single#Chat#10: bob@example.net [PGP bob@example.net] 🛡️ +-------------------------------------------------------------------------------- +Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️] +Msg#11🔒: (Contact#Contact#10): Heyho from DC [FRESH] +Msg#13🔒: Me (Contact#Contact#Self): Sending with DC again √ +-------------------------------------------------------------------------------- From dc5ddf279a509cbf77162ca3009cb0fa00adfa68 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 20 Apr 2025 19:19:11 +0000 Subject: [PATCH 095/381] fix test_contact_get_encrinfo --- src/chat.rs | 2 +- src/contact.rs | 15 +++++++++++---- src/contact/contact_tests.rs | 31 ++++++++++--------------------- src/receive_imf.rs | 1 - src/securejoin.rs | 3 +-- src/tests/verified_chats.rs | 5 ++++- 6 files changed, 27 insertions(+), 30 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index 2a88a754e4..7cb6eb53cc 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -2333,7 +2333,7 @@ impl Chat { } let contact = Contact::get_by_id(context, contact_id).await?; if let Some(fingerprint) = contact.fingerprint() { - r = Some(SyncId::ContactFingerprint(fingerprint.to_string())); + r = Some(SyncId::ContactFingerprint(fingerprint.hex())); } else { r = Some(SyncId::ContactAddr(contact.get_addr().to_string())); } diff --git a/src/contact.rs b/src/contact.rs index 25c61ff400..97729fac7a 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -27,7 +27,9 @@ use crate::config::Config; use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY}; use crate::context::Context; use crate::events::EventType; -use crate::key::{load_self_public_key, load_self_public_key_opt, DcKey, SignedPublicKey}; +use crate::key::{ + load_self_public_key, load_self_public_key_opt, DcKey, Fingerprint, SignedPublicKey, +}; use crate::log::LogExt; use crate::message::MessageState; use crate::mimeparser::AvatarAction; @@ -1258,6 +1260,7 @@ impl Contact { let Some(fingerprint_other) = contact.fingerprint() else { return Ok(stock_str::encr_none(context).await); }; + let fingerprint_other = fingerprint_other.to_string(); let stock_message = stock_str::e2e_available(context).await; @@ -1374,8 +1377,12 @@ impl Contact { /// Returns OpenPGP fingerprint of a contact. /// /// `None` for e-mail contacts. - pub fn fingerprint(&self) -> Option<&str> { - self.fingerprint.as_deref() + pub fn fingerprint(&self) -> Option { + if let Some(fingerprint) = &self.fingerprint { + fingerprint.parse().ok() + } else { + None + } } /// Returns OpenPGP certificate of a contact. @@ -1726,7 +1733,7 @@ WHERE type=? AND id IN ( false => chat::SyncAction::Unblock, }; let sync_id = if let Some(fingerprint) = contact.fingerprint() { - chat::SyncId::ContactFingerprint(fingerprint.to_string()) + chat::SyncId::ContactFingerprint(fingerprint.hex()) } else { chat::SyncId::ContactAddr(contact.addr.clone()) }; diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs index 4d5835a86d..90d74b871c 100644 --- a/src/contact/contact_tests.rs +++ b/src/contact/contact_tests.rs @@ -728,7 +728,9 @@ async fn test_contact_get_color() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_contact_get_encrinfo() -> Result<()> { - let alice = TestContext::new_alice().await; + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; // Return error for special IDs let encrinfo = Contact::get_encrinfo(&alice, ContactId::SELF).await; @@ -736,38 +738,25 @@ async fn test_contact_get_encrinfo() -> Result<()> { let encrinfo = Contact::get_encrinfo(&alice, ContactId::DEVICE).await; assert!(encrinfo.is_err()); - let (contact_bob_id, _modified) = Contact::add_or_lookup( - &alice, - "Bob", - &ContactAddress::new("bob@example.net")?, - Origin::ManuallyCreated, - ) - .await?; - - let encrinfo = Contact::get_encrinfo(&alice, contact_bob_id).await?; + let email_contact_bob_id = alice.add_or_lookup_email_contact_id(bob).await; + let encrinfo = Contact::get_encrinfo(&alice, email_contact_bob_id).await?; assert_eq!(encrinfo, "No encryption"); - let contact = Contact::get_by_id(&alice, contact_bob_id).await?; - assert!(!contact.e2ee_avail(&alice).await?); - let bob = TestContext::new_bob().await; - let chat_alice = bob - .create_chat_with_contact("Alice", "alice@example.org") - .await; - send_text_msg(&bob, chat_alice.id, "Hello".to_string()).await?; - let msg = bob.pop_sent_msg().await; - alice.recv_msg(&msg).await; + let contact = Contact::get_by_id(&alice, email_contact_bob_id).await?; + assert!(!contact.e2ee_avail(&alice).await?); + let contact_bob_id = alice.add_or_lookup_contact_id(bob).await; let encrinfo = Contact::get_encrinfo(&alice, contact_bob_id).await?; assert_eq!( encrinfo, - "End-to-end encryption preferred. + "End-to-end encryption available. Fingerprints: Me (alice@example.org): 2E6F A2CB 23B5 32D7 2863 4B58 64B0 8F61 A9ED 9443 -Bob (bob@example.net): +bob@example.net (bob@example.net): CCCB 5AA9 F6E1 141C 9431 65F1 DB18 B18C BCF7 0487" ); diff --git a/src/receive_imf.rs b/src/receive_imf.rs index b9d3074912..9407eabf2a 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -3056,7 +3056,6 @@ async fn has_verified_encryption( "The message was sent by non-verified contact".to_string(), )); } - let fingerprint: Fingerprint = fingerprint.parse().context("Failed to parse fingerprint")?; let signed_with_verified_key = mimeparser.signatures.contains(&fingerprint); if signed_with_verified_key { diff --git a/src/securejoin.rs b/src/securejoin.rs index 5a038f5ab0..da4dfc4066 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -205,9 +205,8 @@ async fn verify_sender_by_fingerprint( fingerprint: &Fingerprint, contact_id: ContactId, ) -> Result { - let fingerprint = fingerprint.hex(); let contact = Contact::get_by_id(context, contact_id).await?; - let is_verified = contact.fingerprint().is_some_and(|fp| fp == fingerprint); + let is_verified = contact.fingerprint().is_some_and(|fp| &fp == fingerprint); if is_verified { mark_contact_id_as_verified(context, contact_id, contact_id).await?; } diff --git a/src/tests/verified_chats.rs b/src/tests/verified_chats.rs index 7a129908f3..5a5dcd733f 100644 --- a/src/tests/verified_chats.rs +++ b/src/tests/verified_chats.rs @@ -595,7 +595,10 @@ async fn test_outgoing_mua_msg() -> Result<()> { .golden_test_chat(sent.chat_id, "test_outgoing_mua_msg") .await; alice - .golden_test_chat(alice.get_pgp_chat(&bob).await.id, "test_outgoing_mua_msg_pgp") + .golden_test_chat( + alice.get_pgp_chat(&bob).await.id, + "test_outgoing_mua_msg_pgp", + ) .await; Ok(()) From 242b0a3fed2e3d0df4ba7f8cfab5e6f01130e74d Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 21 Apr 2025 06:32:59 +0000 Subject: [PATCH 096/381] fix test_sync_block_before_first_msg --- src/chat/chat_tests.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 51b3a7fe10..27d959ca23 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -3067,23 +3067,24 @@ async fn test_sync_accept_before_first_msg() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_sync_block_before_first_msg() -> Result<()> { - let alice0 = &TestContext::new_alice().await; - let alice1 = &TestContext::new_alice().await; + let mut tcm = TestContextManager::new(); + let alice0 = &tcm.alice().await; + let alice1 = &tcm.alice().await; for a in [alice0, alice1] { a.set_config_bool(Config::SyncMsgs, true).await?; } - let bob = TestContext::new_bob().await; + let bob = &tcm.bob().await; let ba_chat = bob.create_chat(alice0).await; let sent_msg = bob.send_text(ba_chat.id, "hi").await; let rcvd_msg = alice0.recv_msg(&sent_msg).await; let a0b_chat_id = rcvd_msg.chat_id; let a0b_contact_id = rcvd_msg.from_id; - assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Request); + assert_eq!(Chat::load_from_db(alice0, a0b_chat_id).await?.blocked, Blocked::Request); a0b_chat_id.block(alice0).await?; let a0b_contact = Contact::get_by_id(alice0, a0b_contact_id).await?; assert_eq!(a0b_contact.origin, Origin::IncomingUnknownFrom); - assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Yes); + assert_eq!(Chat::load_from_db(alice0, a0b_chat_id).await?.blocked, Blocked::Yes); sync(alice0, alice1).await; let alice1_contacts = Contact::get_all(alice1, 0, None).await?; @@ -3093,9 +3094,9 @@ async fn test_sync_block_before_first_msg() -> Result<()> { let a1b_contact_id = rcvd_msg.from_id; let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?; assert_eq!(a1b_contact.origin, Origin::IncomingUnknownFrom); - let a1b_chat = alice1.get_chat(&bob).await; - assert_eq!(a1b_chat.blocked, Blocked::Yes); - assert_eq!(rcvd_msg.chat_id, a1b_chat.id); + let ChatIdBlocked {id: a1b_chat_id, blocked: a1b_chat_blocked} = ChatIdBlocked::lookup_by_contact(alice1, a1b_contact_id).await?.unwrap(); + assert_eq!(a1b_chat_blocked, Blocked::Yes); + assert_eq!(rcvd_msg.chat_id, a1b_chat_id); Ok(()) } From e4912c388ca675fdd339e205595d79ea6478f90e Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 21 Apr 2025 08:00:57 +0000 Subject: [PATCH 097/381] fix test_saved_msgs_not_added_to_shared_chats --- src/chat/chat_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 27d959ca23..d8a7c9c99d 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -2313,7 +2313,7 @@ async fn test_saved_msgs_not_added_to_shared_chats() -> Result<()> { assert_eq!(shared_chats.len(), 1); assert_eq!( shared_chats.get_chat_id(0).unwrap(), - bob.get_chat(&alice).await.id + bob.get_pgp_chat(&alice).await.id ); Ok(()) From f3277c4496964edd942b3ba67256ba0dc8b3e9c9 Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 21 Apr 2025 06:32:59 +0000 Subject: [PATCH 098/381] remove test_verified_oneonone_chat_enable_disable --- src/tests/verified_chats.rs | 57 ------------------------------------- 1 file changed, 57 deletions(-) diff --git a/src/tests/verified_chats.rs b/src/tests/verified_chats.rs index 5a5dcd733f..15554dece9 100644 --- a/src/tests/verified_chats.rs +++ b/src/tests/verified_chats.rs @@ -273,63 +273,6 @@ async fn test_degrade_verified_oneonone_chat() -> Result<()> { Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_verified_oneonone_chat_enable_disable() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - enable_verified_oneonone_chats(&[&alice, &bob]).await; - - // Alice & Bob verify each other - mark_as_verified(&alice, &bob).await; - mark_as_verified(&bob, &alice).await; - - let chat = alice.create_chat(&bob).await; - assert!(chat.is_protected()); - - for alice_accepts_breakage in [true, false] { - SystemTime::shift(std::time::Duration::from_secs(300)); - // Bob uses Thunderbird to send a message - receive_imf( - &alice, - format!( - "From: Bob \n\ - To: alice@example.org\n\ - Message-ID: <1234-2{alice_accepts_breakage}@example.org>\n\ - \n\ - Message from Thunderbird\n" - ) - .as_bytes(), - false, - ) - .await?; - - let chat = alice.get_chat(&bob).await; - assert!(!chat.is_protected()); - assert!(chat.is_protection_broken()); - - if alice_accepts_breakage { - tcm.section("Alice clicks 'Accept' on the input-bar-dialog"); - chat.id.accept(&alice).await?; - let chat = alice.get_chat(&bob).await; - assert!(!chat.is_protected()); - assert!(!chat.is_protection_broken()); - } - - // Bob sends a message from DC again - tcm.send_recv(&bob, &alice, "Hello from DC").await; - let chat = alice.get_chat(&bob).await; - assert!(chat.is_protected()); - assert!(!chat.is_protection_broken()); - } - - alice - .golden_test_chat(chat.id, "test_verified_oneonone_chat_enable_disable") - .await; - - Ok(()) -} - /// Messages with old timestamps are difficult for verified chats: /// - They must not be sorted over a protection-changed info message. /// That's what `test_old_message_2` tests From b1882b435646156737f5800bba34e84735288e3b Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 21 Apr 2025 06:32:59 +0000 Subject: [PATCH 099/381] rename get_chat() into get_email_chat() --- src/receive_imf/receive_imf_tests.rs | 6 +-- src/test_utils.rs | 2 +- src/tests/verified_chats.rs | 78 ++-------------------------- 3 files changed, 7 insertions(+), 79 deletions(-) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 3d6cecccc7..d4816b3384 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -3359,8 +3359,8 @@ async fn test_outgoing_private_reply_multidevice() -> Result<()> { // =============== Alice's second device receives the message =============== let received = alice2.get_last_msg().await; - // That's a regression test for https://github.com/deltachat/deltachat-core-rust/issues/2949: - assert_eq!(received.chat_id, alice2.get_chat(&bob).await.id); + // That's a regression test for https://github.com/chatmail/core/issues/2949: + assert_eq!(received.chat_id, alice2.get_pgp_chat(&bob).await.id); let alice2_bob_contact = alice2.add_or_lookup_contact(&bob).await; assert_eq!(received.from_id, ContactId::SELF); @@ -5122,7 +5122,7 @@ async fn test_prefer_references_to_downloaded_msgs() -> Result<()> { let received = bob.recv_msg(&sent).await; assert_eq!(received.download_state, DownloadState::Available); assert_ne!(received.chat_id, bob_chat_id); - assert_eq!(received.chat_id, bob.get_chat(alice).await.id); + assert_eq!(received.chat_id, bob.get_pgp_chat(alice).await.id); let mut msg = Message::new(Viewtype::File); msg.set_file_from_bytes(alice, "file", file_bytes, None)?; diff --git a/src/test_utils.rs b/src/test_utils.rs index e0404e336c..3813e36764 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -791,7 +791,7 @@ impl TestContext { /// /// This first creates a contact using the configured details on the other account, then /// gets the 1:1 chat with this contact. - pub async fn get_chat(&self, other: &TestContext) -> Chat { + pub async fn get_email_chat(&self, other: &TestContext) -> Chat { let contact = self.add_or_lookup_email_contact(other).await; let chat_id = ChatIdBlocked::lookup_by_contact(&self.ctx, contact.id) diff --git a/src/tests/verified_chats.rs b/src/tests/verified_chats.rs index 15554dece9..7dd48941c1 100644 --- a/src/tests/verified_chats.rs +++ b/src/tests/verified_chats.rs @@ -264,7 +264,7 @@ async fn test_degrade_verified_oneonone_chat() -> Result<()> { assert_eq!(msg0.text, enabled); assert_eq!(msg0.param.get_cmd(), SystemMessage::ChatProtectionEnabled); - let email_chat = alice.get_chat(&bob).await; + let email_chat = alice.get_email_chat(&bob).await; assert!(!email_chat.is_encrypted(&alice).await?); let email_msg = get_chat_msg(&alice, email_chat.id, 0, 1).await; assert_eq!(email_msg.text, "hello".to_string()); @@ -398,7 +398,7 @@ async fn test_old_message_3() -> Result<()> { .await?; alice - .golden_test_chat(alice.get_chat(&bob).await.id, "test_old_message_3") + .golden_test_chat(alice.get_pgp_chat(&bob).await.id, "test_old_message_3") .await; Ok(()) @@ -600,78 +600,6 @@ async fn test_reply() -> Result<()> { Ok(()) } -/// Regression test for the following bug: -/// -/// - Scan your chat partner's QR Code -/// - They change devices -/// - They send you a message -/// - Without accepting the encryption downgrade, scan your chat partner's QR Code again -/// -/// -> The re-verification fails. -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_break_protection_then_verify_again() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - enable_verified_oneonone_chats(&[&alice, &bob]).await; - - // Cave: Bob can't write a message to Alice here. - // If he did, alice would increase his peerstate's last_seen timestamp. - // Then, after Bob reinstalls DC, alice's `if message_time > last_seen*` - // checks would return false (there are many checks of this form in peerstate.rs). - // Therefore, during the securejoin, Alice wouldn't accept the new key - // and reject the securejoin. - - mark_as_verified(&alice, &bob).await; - mark_as_verified(&bob, &alice).await; - - alice.create_chat(&bob).await; - assert_verified(&alice, &bob, ProtectionStatus::Protected).await; - let chats = Chatlist::try_load(&alice, DC_GCL_FOR_FORWARDING, None, None).await?; - assert_eq!(chats.len(), 1); - - tcm.section("Bob reinstalls DC"); - drop(bob); - let bob_new = tcm.unconfigured().await; - enable_verified_oneonone_chats(&[&bob_new]).await; - bob_new.configure_addr("bob@example.net").await; - e2ee::ensure_secret_key_exists(&bob_new).await?; - - tcm.send_recv(&bob_new, &alice, "I have a new device").await; - - let contact = alice.add_or_lookup_contact(&bob_new).await; - assert_eq!( - contact.is_verified(&alice).await.unwrap(), - // Bob sent a message with a new key, so he most likely doesn't have - // the old key anymore. This means that Alice's device should show - // him as unverified: - false - ); - let chat = alice.get_chat(&bob_new).await; - assert_eq!(chat.is_protected(), false); - assert_eq!(chat.is_protection_broken(), true); - let chats = Chatlist::try_load(&alice, DC_GCL_FOR_FORWARDING, None, None).await?; - assert_eq!(chats.len(), 1); - - { - let alice_bob_chat = alice.get_chat(&bob_new).await; - assert!(!alice_bob_chat.can_send(&alice).await?); - - // Alice's UI should still be able to save a draft, which Alice started to type right when she got Bob's message: - let mut msg = Message::new_text("Draftttt".to_string()); - alice_bob_chat.id.set_draft(&alice, Some(&mut msg)).await?; - assert_eq!( - alice_bob_chat.id.get_draft(&alice).await?.unwrap().text, - "Draftttt" - ); - } - - tcm.execute_securejoin(&alice, &bob_new).await; - assert_verified(&alice, &bob_new, ProtectionStatus::Protected).await; - - Ok(()) -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_message_from_old_dc_setup() -> Result<()> { let mut tcm = TestContextManager::new(); @@ -701,7 +629,7 @@ async fn test_message_from_old_dc_setup() -> Result<()> { let contact = alice.add_or_lookup_contact(bob).await; // The outdated Bob's Autocrypt header isn't applied, so the verification preserves. assert!(contact.is_verified(alice).await.unwrap()); - let chat = alice.get_chat(bob).await; + let chat = alice.get_pgp_chat(bob).await; assert!(chat.is_protected()); assert_eq!(chat.is_protection_broken(), false); let protection_msg = alice.get_last_msg().await; From 2eb03c474f110b488f9e6c68f2cf987f5699c459 Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 21 Apr 2025 17:28:45 +0000 Subject: [PATCH 100/381] test: add test_no_email_contacts_in_group_chats --- src/chat/chat_tests.rs | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index d8a7c9c99d..ecf005f457 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -3080,11 +3080,17 @@ async fn test_sync_block_before_first_msg() -> Result<()> { let rcvd_msg = alice0.recv_msg(&sent_msg).await; let a0b_chat_id = rcvd_msg.chat_id; let a0b_contact_id = rcvd_msg.from_id; - assert_eq!(Chat::load_from_db(alice0, a0b_chat_id).await?.blocked, Blocked::Request); + assert_eq!( + Chat::load_from_db(alice0, a0b_chat_id).await?.blocked, + Blocked::Request + ); a0b_chat_id.block(alice0).await?; let a0b_contact = Contact::get_by_id(alice0, a0b_contact_id).await?; assert_eq!(a0b_contact.origin, Origin::IncomingUnknownFrom); - assert_eq!(Chat::load_from_db(alice0, a0b_chat_id).await?.blocked, Blocked::Yes); + assert_eq!( + Chat::load_from_db(alice0, a0b_chat_id).await?.blocked, + Blocked::Yes + ); sync(alice0, alice1).await; let alice1_contacts = Contact::get_all(alice1, 0, None).await?; @@ -3094,7 +3100,12 @@ async fn test_sync_block_before_first_msg() -> Result<()> { let a1b_contact_id = rcvd_msg.from_id; let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?; assert_eq!(a1b_contact.origin, Origin::IncomingUnknownFrom); - let ChatIdBlocked {id: a1b_chat_id, blocked: a1b_chat_blocked} = ChatIdBlocked::lookup_by_contact(alice1, a1b_contact_id).await?.unwrap(); + let ChatIdBlocked { + id: a1b_chat_id, + blocked: a1b_chat_blocked, + } = ChatIdBlocked::lookup_by_contact(alice1, a1b_contact_id) + .await? + .unwrap(); assert_eq!(a1b_chat_blocked, Blocked::Yes); assert_eq!(rcvd_msg.chat_id, a1b_chat_id); Ok(()) @@ -4139,3 +4150,25 @@ async fn test_oneone_gossip() -> Result<()> { Ok(()) } + +/// Tests that email contacts cannot be added to encrypted group chats. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_no_email_contacts_in_group_chats() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let charlie = &tcm.charlie().await; + + let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?; + let pgp_bob_contact_id = alice.add_or_lookup_contact_id(bob).await; + let email_charlie_contact_id = alice.add_or_lookup_email_contact_id(charlie).await; + + // PGP-contact should be added successfully. + add_contact_to_chat(alice, chat_id, pgp_bob_contact_id).await?; + + // Adding email-contact should fail. + let res = add_contact_to_chat(alice, chat_id, email_charlie_contact_id).await; + assert!(res.is_err()); + + Ok(()) +} From 78eba1dd7bbd9f5bf2a82c74e21de63d8a513d71 Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 21 Apr 2025 20:45:35 +0000 Subject: [PATCH 101/381] test_no_pgp_contacts_in_adhoc_chats --- src/chat/chat_tests.rs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index ecf005f457..57faf3c943 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -4172,3 +4172,39 @@ async fn test_no_email_contacts_in_group_chats() -> Result<()> { Ok(()) } + +/// Tests that PGP-contacts cannot be added to ad-hoc groups. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_no_pgp_contacts_in_adhoc_chats() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let charlie = &tcm.charlie().await; + + let chat_id = receive_imf( + alice, + b"Subject: Email thread\r\n\ + From: alice@example.org\r\n\ + To: Bob , Fiona \r\n\ + Date: Mon, 2 Dec 2023 16:59:39 +0000\r\n\ + Message-ID: \r\n\ + \r\n\ + Starting a new thread\r\n", + false, + ) + .await? + .unwrap() + .chat_id; + + let email_bob_contact_id = alice.add_or_lookup_email_contact_id(bob).await; + let pgp_charlie_contact_id = alice.add_or_lookup_contact_id(charlie).await; + + // Email-contact should be added successfully. + add_contact_to_chat(alice, chat_id, email_bob_contact_id).await?; + + // Adding PGP-contact should fail. + let res = add_contact_to_chat(alice, chat_id, pgp_charlie_contact_id).await; + assert!(res.is_err()); + + Ok(()) +} From 5c0562af1cbca515add11cc7df7069bc3b8edabe Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 21 Apr 2025 20:41:05 +0000 Subject: [PATCH 102/381] Encrypted contacts must go to encrypted chats --- src/chat.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/chat.rs b/src/chat.rs index 7cb6eb53cc..20ad9359e0 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -3882,6 +3882,10 @@ pub(crate) async fn add_contact_to_chat_ex( chat.typ != Chattype::Broadcast || contact_id != ContactId::SELF, "Cannot add SELF to broadcast." ); + ensure!( + chat.is_encrypted(context).await? == contact.is_pgp_contact(), + "Only PGP-contacts can be added to encrypted chats" + ); if !chat.is_self_in_chat(context).await? { context.emit_event(EventType::ErrorSelfNotInGroup( From 9bf093a675f28e6299e9c93d2086c1ee52d0ed75 Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 21 Apr 2025 22:03:12 +0000 Subject: [PATCH 103/381] fix chat::chat_tests::test_shall_attach_selfavatar --- src/chat/chat_tests.rs | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 57faf3c943..9f0a0cf3bb 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -1490,25 +1490,22 @@ async fn test_create_same_chat_twice() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_shall_attach_selfavatar() -> Result<()> { - let t = TestContext::new().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; - assert!(!shall_attach_selfavatar(&t, chat_id).await?); + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; - let (contact_id, _) = Contact::add_or_lookup( - &t, - "", - &ContactAddress::new("foo@bar.org")?, - Origin::IncomingUnknownTo, - ) - .await?; - add_contact_to_chat(&t, chat_id, contact_id).await?; - assert!(shall_attach_selfavatar(&t, chat_id).await?); + let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foo").await?; + assert!(!shall_attach_selfavatar(alice, chat_id).await?); + + let contact_id = alice.add_or_lookup_contact_id(bob).await; + add_contact_to_chat(alice, chat_id, contact_id).await?; + assert!(shall_attach_selfavatar(alice, chat_id).await?); - chat_id.set_selfavatar_timestamp(&t, time()).await?; - assert!(!shall_attach_selfavatar(&t, chat_id).await?); + chat_id.set_selfavatar_timestamp(alice, time()).await?; + assert!(!shall_attach_selfavatar(alice, chat_id).await?); - t.set_config(Config::Selfavatar, None).await?; // setting to None also forces re-sending - assert!(shall_attach_selfavatar(&t, chat_id).await?); + alice.set_config(Config::Selfavatar, None).await?; // setting to None also forces re-sending + assert!(shall_attach_selfavatar(alice, chat_id).await?); Ok(()) } From c77f523594f0a31a6171c7bc5f935bdcea0ba117 Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 21 Apr 2025 22:36:52 +0000 Subject: [PATCH 104/381] tests::aeap::test_aeap_replay_attack --- src/tests/aeap.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/tests/aeap.rs b/src/tests/aeap.rs index 7067a86805..de6e54bf1c 100644 --- a/src/tests/aeap.rs +++ b/src/tests/aeap.rs @@ -209,6 +209,7 @@ async fn test_aeap_replay_attack() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = tcm.alice().await; let bob = tcm.bob().await; + let fiona = tcm.fiona().await; tcm.send_recv_accept(&alice, &bob, "Hi").await; tcm.send_recv(&bob, &alice, "Hi back").await; @@ -216,7 +217,8 @@ async fn test_aeap_replay_attack() -> Result<()> { let group = chat::create_group_chat(&bob, chat::ProtectionStatus::Unprotected, "Group 0").await?; - let bob_alice_contact = Contact::create(&bob, "Alice", "alice@example.org").await?; + let bob_alice_contact = bob.add_or_lookup_contact_id(&alice).await; + let bob_fiona_contact = bob.add_or_lookup_contact_id(&fiona).await; chat::add_contact_to_chat(&bob, group, bob_alice_contact).await?; // Alice sends a message which Bob doesn't receive or something @@ -238,7 +240,6 @@ async fn test_aeap_replay_attack() -> Result<()> { // Check that no transition was done assert!(chat::is_contact_in_chat(&bob, group, bob_alice_contact).await?); - let bob_fiona_contact = Contact::create(&bob, "", "fiona@example.net").await?; assert!(!chat::is_contact_in_chat(&bob, group, bob_fiona_contact).await?); Ok(()) From fce62d2be499e69ef048cdd2d4b0c5dc23b0ce7e Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 21 Apr 2025 23:50:13 +0000 Subject: [PATCH 105/381] receive_imf::receive_imf_tests::test_read_receipt_and_unarchive fixed --- src/receive_imf/receive_imf_tests.rs | 49 ++++++++++++++-------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index d4816b3384..5f38ba2b85 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -273,31 +273,32 @@ async fn test_adhoc_groups_merge() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_read_receipt_and_unarchive() -> Result<()> { - // create alice's account - let t = TestContext::new_alice().await; + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; - let bob_id = Contact::create(&t, "bob", "bob@example.com").await?; - let one2one_id = ChatId::create_for_contact(&t, bob_id).await?; - one2one_id - .set_visibility(&t, ChatVisibility::Archived) + let bob_id = alice.add_or_lookup_contact_id(bob).await; + let one2one = alice.create_chat(bob).await; + one2one.id + .set_visibility(alice, ChatVisibility::Archived) .await .unwrap(); - let one2one = Chat::load_from_db(&t, one2one_id).await?; - assert!(one2one.get_visibility() == ChatVisibility::Archived); + let one2one = Chat::load_from_db(alice, one2one.id).await?; + assert_eq!(one2one.get_visibility(), ChatVisibility::Archived); // create a group with bob, archive group - let group_id = chat::create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; - chat::add_contact_to_chat(&t, group_id, bob_id).await?; - assert_eq!(chat::get_chat_msgs(&t, group_id).await.unwrap().len(), 0); + let group_id = chat::create_group_chat(alice, ProtectionStatus::Unprotected, "foo").await?; + chat::add_contact_to_chat(alice, group_id, bob_id).await?; + assert_eq!(chat::get_chat_msgs(alice, group_id).await.unwrap().len(), 0); group_id - .set_visibility(&t, ChatVisibility::Archived) + .set_visibility(alice, ChatVisibility::Archived) .await?; - let group = Chat::load_from_db(&t, group_id).await?; - assert!(group.get_visibility() == ChatVisibility::Archived); + let group = Chat::load_from_db(alice, group_id).await?; + assert_eq!(group.get_visibility(), ChatVisibility::Archived); // everything archived, chatlist should be empty assert_eq!( - Chatlist::try_load(&t, DC_GCL_NO_SPECIALS, None, None) + Chatlist::try_load(alice, DC_GCL_NO_SPECIALS, None, None) .await? .len(), 0 @@ -305,7 +306,7 @@ async fn test_read_receipt_and_unarchive() -> Result<()> { // send a message to group with bob receive_imf( - &t, + alice, format!( "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ From: alice@example.org\n\ @@ -325,16 +326,16 @@ async fn test_read_receipt_and_unarchive() -> Result<()> { false, ) .await?; - let msg = get_chat_msg(&t, group_id, 0, 1).await; + let msg = get_chat_msg(alice, group_id, 0, 1).await; assert_eq!(msg.is_dc_message, MessengerMessage::Yes); assert_eq!(msg.text, "hello"); assert_eq!(msg.state, MessageState::OutDelivered); - let group = Chat::load_from_db(&t, group_id).await?; + let group = Chat::load_from_db(alice, group_id).await?; assert!(group.get_visibility() == ChatVisibility::Normal); // bob sends a read receipt to the group receive_imf( - &t, + alice, format!( "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ From: bob@example.com\n\ @@ -369,19 +370,19 @@ async fn test_read_receipt_and_unarchive() -> Result<()> { false, ) .await?; - assert_eq!(chat::get_chat_msgs(&t, group_id).await?.len(), 1); - let msg = message::Message::load_from_db(&t, msg.id).await?; + assert_eq!(chat::get_chat_msgs(alice, group_id).await?.len(), 1); + let msg = message::Message::load_from_db(alice, msg.id).await?; assert_eq!(msg.state, MessageState::OutMdnRcvd); // check, the read-receipt has not unarchived the one2one assert_eq!( - Chatlist::try_load(&t, DC_GCL_NO_SPECIALS, None, None) + Chatlist::try_load(alice, DC_GCL_NO_SPECIALS, None, None) .await? .len(), 1 ); - let one2one = Chat::load_from_db(&t, one2one_id).await?; - assert!(one2one.get_visibility() == ChatVisibility::Archived); + let one2one = Chat::load_from_db(alice, one2one.id).await?; + assert_eq!(one2one.get_visibility(), ChatVisibility::Archived); Ok(()) } From ae3ae964e499b7b07baff679d5852ab38831f890 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 22 Apr 2025 00:21:15 +0000 Subject: [PATCH 106/381] fix test_chat_assignment_adhoc --- src/receive_imf/receive_imf_tests.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 5f38ba2b85..5d222ccfb4 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -2754,8 +2754,10 @@ Reply to all"#, /// headers. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_chat_assignment_adhoc() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + let fiona = tcm.fiona().await; let first_thread_mime = br#"Subject: First thread Message-ID: first@example.org @@ -2785,8 +2787,8 @@ Second thread."#; let bob_second_msg = bob.get_last_msg().await; // Messages go to separate chats both for Alice and Bob. - assert!(alice_first_msg.chat_id != alice_second_msg.chat_id); - assert!(bob_first_msg.chat_id != bob_second_msg.chat_id); + assert_ne!(alice_first_msg.chat_id, alice_second_msg.chat_id); + assert_ne!(bob_first_msg.chat_id, bob_second_msg.chat_id); // Alice replies to both chats. Bob receives two messages and assigns them to corresponding // chats. @@ -2805,8 +2807,7 @@ Second thread."#; assert_eq!(bob_second_reply.chat_id, bob_second_msg.chat_id); // Alice adds Fiona to both ad hoc groups. - let fiona = TestContext::new_fiona().await; - let alice_fiona_contact = alice.add_or_lookup_contact(&fiona).await; + let alice_fiona_contact = alice.add_or_lookup_email_contact(&fiona).await; let alice_fiona_contact_id = alice_fiona_contact.id; chat::add_contact_to_chat(&alice, alice_first_msg.chat_id, alice_fiona_contact_id).await?; @@ -2819,7 +2820,7 @@ Second thread."#; // Fiona was added to two separate chats and should see two separate chats, even though they // don't have different group IDs to distinguish them. - assert!(fiona_first_invite.chat_id != fiona_second_invite.chat_id); + assert_ne!(fiona_first_invite.chat_id, fiona_second_invite.chat_id); Ok(()) } From 98da6ac0fb22f39828d9abcecbd29363d9747c67 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 22 Apr 2025 01:18:22 +0000 Subject: [PATCH 107/381] fix chat::chat_tests::test_chat_get_encryption_info --- src/chat.rs | 33 +++++++---------------- src/chat/chat_tests.rs | 60 ++++++++++++++++++------------------------ 2 files changed, 35 insertions(+), 58 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index 20ad9359e0..387a19e068 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1322,8 +1322,12 @@ impl ChatId { /// /// To get more verbose summary for a contact, including its key fingerprint, use [`Contact::get_encrinfo`]. pub async fn get_encryption_info(self, context: &Context) -> Result { - let mut ret_available = String::new(); - let mut ret_reset = String::new(); + let chat = Chat::load_from_db(context, self).await?; + if !chat.is_encrypted(context).await? { + return Ok(stock_str::encr_none(context).await); + } + + let mut ret = stock_str::e2e_available(context).await + "\n"; for contact_id in get_chat_contacts(context, self) .await? @@ -1332,28 +1336,9 @@ impl ChatId { { let contact = Contact::get_by_id(context, *contact_id).await?; let addr = contact.get_addr(); - if contact.is_pgp_contact() { - ret_available += &format!("{addr}\n"); - } else { - ret_reset += &format!("{addr}\n"); - } - } - - let mut ret = String::new(); - if !ret_reset.is_empty() { - ret += &stock_str::encr_none(context).await; - ret.push(':'); - ret.push('\n'); - ret += &ret_reset; - } - if !ret_available.is_empty() { - if !ret.is_empty() { - ret.push('\n'); - } - ret += &stock_str::e2e_available(context).await; - ret.push(':'); - ret.push('\n'); - ret += &ret_available; + debug_assert!(contact.is_pgp_contact()); + let fingerprint = contact.fingerprint().context("Contact does not have a fingerprint in encrypted chat")?; + ret += &format!("\n{addr}\n{fingerprint}\n"); } Ok(ret.trim().to_string()) diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 9f0a0cf3bb..5a090fdea1 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -2709,53 +2709,45 @@ async fn test_create_for_contact_with_blocked() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_chat_get_encryption_info() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let fiona = &tcm.fiona().await; - let contact_bob = Contact::create(&alice, "Bob", "bob@example.net").await?; - let contact_fiona = Contact::create(&alice, "", "fiona@example.net").await?; + let contact_bob = alice.add_or_lookup_contact_id(bob).await; + let contact_fiona = alice.add_or_lookup_contact_id(fiona).await; - let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?; - assert_eq!(chat_id.get_encryption_info(&alice).await?, ""); + let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?; + assert_eq!(chat_id.get_encryption_info(alice).await?, "End-to-end encryption available"); - add_contact_to_chat(&alice, chat_id, contact_bob).await?; + add_contact_to_chat(alice, chat_id, contact_bob).await?; assert_eq!( chat_id.get_encryption_info(&alice).await?, - "No encryption:\n\ - bob@example.net" + "End-to-end encryption available\n\ + \n\ + bob@example.net\n\ + CCCB 5AA9 F6E1 141C 9431\n\ + 65F1 DB18 B18C BCF7 0487" ); add_contact_to_chat(&alice, chat_id, contact_fiona).await?; assert_eq!( chat_id.get_encryption_info(&alice).await?, - "No encryption:\n\ - fiona@example.net\n\ - bob@example.net" + "End-to-end encryption available\n\ + \n\ + fiona@example.net\n\ + C8BA 50BF 4AC1 2FAF 38D7\n\ + F657 DDFC 8E9F 3C79 9195\n\ + \n\ + bob@example.net\n\ + CCCB 5AA9 F6E1 141C 9431\n\ + 65F1 DB18 B18C BCF7 0487" ); - let direct_chat = bob.create_chat(&alice).await; - send_text_msg(&bob, direct_chat.id, "Hello!".to_string()).await?; - alice.recv_msg(&bob.pop_sent_msg().await).await; - + let email_chat = alice.create_email_chat(bob).await; assert_eq!( - chat_id.get_encryption_info(&alice).await?, - "No encryption:\n\ - fiona@example.net\n\ - \n\ - End-to-end encryption available:\n\ - bob@example.net" - ); - - send_text_msg(&bob, direct_chat.id, "Hello!".to_string()).await?; - alice.recv_msg(&bob.pop_sent_msg().await).await; - - assert_eq!( - chat_id.get_encryption_info(&alice).await?, - "No encryption:\n\ - fiona@example.net\n\ - \n\ - End-to-end encryption available:\n\ - bob@example.net" + email_chat.id.get_encryption_info(&alice).await?, + "No encryption" ); Ok(()) From 4b3877681408363726a0a93c9ac7f7e03966488e Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 22 Apr 2025 01:19:57 +0000 Subject: [PATCH 108/381] remove test_resend_opportunistically_encryption --- src/chat/chat_tests.rs | 37 ------------------------------------- 1 file changed, 37 deletions(-) diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 5a090fdea1..6d3f58d0cb 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -2472,43 +2472,6 @@ async fn test_resend_foreign_message_fails() -> Result<()> { Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_resend_opportunistically_encryption() -> Result<()> { - // Alice creates group with Bob and sends an initial message - let alice = TestContext::new_alice().await; - let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?; - add_contact_to_chat( - &alice, - alice_grp, - Contact::create(&alice, "", "bob@example.net").await?, - ) - .await?; - let sent1 = alice.send_text(alice_grp, "alice->bob").await; - - // Bob now can send an encrypted message - let bob = TestContext::new_bob().await; - let msg = bob.recv_msg(&sent1).await; - assert!(!msg.get_showpadlock()); - - msg.chat_id.accept(&bob).await?; - let sent2 = bob.send_text(msg.chat_id, "bob->alice").await; - let msg = bob.get_last_msg().await; - assert!(msg.get_showpadlock()); - - // Bob adds Claire and resends his last message: this will drop encryption in opportunistic chats - add_contact_to_chat( - &bob, - msg.chat_id, - Contact::create(&bob, "", "claire@example.org").await?, - ) - .await?; - let _sent3 = bob.pop_sent_msg().await; - resend_msgs(&bob, &[sent2.sender_msg_id]).await?; - let _sent4 = bob.pop_sent_msg().await; - - Ok(()) -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_resend_info_message_fails() -> Result<()> { let mut tcm = TestContextManager::new(); From 32813350b540439eab206ad9222ef7a104f5edd2 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 22 Apr 2025 16:16:41 +0000 Subject: [PATCH 109/381] cleanup --- src/chat.rs | 4 +++- src/chat/chat_tests.rs | 5 ++++- src/receive_imf/receive_imf_tests.rs | 3 ++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index 387a19e068..e8c46ed368 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1337,7 +1337,9 @@ impl ChatId { let contact = Contact::get_by_id(context, *contact_id).await?; let addr = contact.get_addr(); debug_assert!(contact.is_pgp_contact()); - let fingerprint = contact.fingerprint().context("Contact does not have a fingerprint in encrypted chat")?; + let fingerprint = contact + .fingerprint() + .context("Contact does not have a fingerprint in encrypted chat")?; ret += &format!("\n{addr}\n{fingerprint}\n"); } diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 6d3f58d0cb..4898ce2a7f 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -2681,7 +2681,10 @@ async fn test_chat_get_encryption_info() -> Result<()> { let contact_fiona = alice.add_or_lookup_contact_id(fiona).await; let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?; - assert_eq!(chat_id.get_encryption_info(alice).await?, "End-to-end encryption available"); + assert_eq!( + chat_id.get_encryption_info(alice).await?, + "End-to-end encryption available" + ); add_contact_to_chat(alice, chat_id, contact_bob).await?; assert_eq!( diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 5d222ccfb4..6ce6502d9c 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -279,7 +279,8 @@ async fn test_read_receipt_and_unarchive() -> Result<()> { let bob_id = alice.add_or_lookup_contact_id(bob).await; let one2one = alice.create_chat(bob).await; - one2one.id + one2one + .id .set_visibility(alice, ChatVisibility::Archived) .await .unwrap(); From 9beb7f503b1711a4899dc5ff38a7779029308130 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 22 Apr 2025 16:16:41 +0000 Subject: [PATCH 110/381] fix ASM --- src/imex/key_transfer.rs | 11 +++++++---- src/mimefactory.rs | 12 ++++++++++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/imex/key_transfer.rs b/src/imex/key_transfer.rs index 88f61742ed..7feed5448d 100644 --- a/src/imex/key_transfer.rs +++ b/src/imex/key_transfer.rs @@ -290,10 +290,12 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_key_transfer() -> Result<()> { - let alice = TestContext::new_alice().await; + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + tcm.section("Alice sends Autocrypt setup message"); alice.set_config(Config::BccSelf, Some("0")).await?; - let setup_code = initiate_key_transfer(&alice).await?; + let setup_code = initiate_key_transfer(alice).await?; // Test that sending Autocrypt Setup Message enables `bcc_self`. assert_eq!(alice.get_config_bool(Config::BccSelf).await?, true); @@ -301,8 +303,8 @@ mod tests { // Get Autocrypt Setup Message. let sent = alice.pop_sent_msg().await; - // Alice sets up a second device. - let alice2 = TestContext::new().await; + tcm.section("Alice sets up a second device"); + let alice2 = &tcm.unconfigured().await; alice2.set_name("alice2"); alice2.configure_addr("alice@example.org").await; alice2.recv_msg(&sent).await; @@ -314,6 +316,7 @@ mod tests { ); // Transfer the key. + tcm.section("Alice imports a key from Autocrypt Setup Message"); alice2.set_config(Config::BccSelf, Some("0")).await?; continue_key_transfer(&alice2, msg.id, &setup_code).await?; assert_eq!(alice2.get_config_bool(Config::BccSelf).await?, true); diff --git a/src/mimefactory.rs b/src/mimefactory.rs index ee9ef57a72..a1b96a5ef9 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -197,8 +197,16 @@ impl MimeFactory { if chat.is_self_talk() { to.push((from_displayname.to_string(), from_addr.to_string())); - // Encrypt, but only to self. - encryption_certificates = Some(Vec::new()); + encryption_certificates = if msg + .param + .get_bool(Param::ForcePlaintext) + .unwrap_or_default() + { + None + } else { + // Encrypt, but only to self. + Some(Vec::new()) + }; } else if chat.is_mailing_list() { let list_post = chat .param From 6474accc2a3643c7b8d90e202184caac79c5c175 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 22 Apr 2025 17:54:37 +0000 Subject: [PATCH 111/381] fix test_unencrypted_quote_encrypted_message --- src/message/message_tests.rs | 53 ++++++++++++++---------------------- 1 file changed, 20 insertions(+), 33 deletions(-) diff --git a/src/message/message_tests.rs b/src/message/message_tests.rs index c62d6d2584..453c88defc 100644 --- a/src/message/message_tests.rs +++ b/src/message/message_tests.rs @@ -183,6 +183,8 @@ async fn test_no_quote() { assert!(msg.quoted_message(bob).await.unwrap().is_none()); } +/// Tests that quote of encrypted message +/// cannot be sent unencrypted. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_unencrypted_quote_encrypted_message() -> Result<()> { let mut tcm = TestContextManager::new(); @@ -190,40 +192,25 @@ async fn test_unencrypted_quote_encrypted_message() -> Result<()> { let alice = &tcm.alice().await; let bob = &tcm.bob().await; - let alice_group = alice - .create_group_with_members(ProtectionStatus::Unprotected, "Group chat", &[bob]) - .await; - let sent = alice.send_text(alice_group, "Hi! I created a group").await; - let bob_received_message = bob.recv_msg(&sent).await; + tcm.section("Bob sends encrypted message to Alice"); + let alice_chat = alice.create_chat(bob).await; + let sent = alice.send_text(alice_chat.id, "Hi! This is encrypted.").await; - let bob_group = bob_received_message.chat_id; - bob_group.accept(bob).await?; - let sent = bob.send_text(bob_group, "Encrypted message").await; - let alice_received_message = alice.recv_msg(&sent).await; - assert!(alice_received_message.get_showpadlock()); - - // Alice adds contact without key so chat becomes unencrypted. - let alice_flubby_contact_id = Contact::create(alice, "Flubby", "flubby@example.org").await?; - add_contact_to_chat(alice, alice_group, alice_flubby_contact_id).await?; - - // Alice quotes encrypted message in unencrypted chat. - let mut msg = Message::new_text("unencrypted".to_string()); - msg.set_quote(alice, Some(&alice_received_message)).await?; - chat::send_msg(alice, alice_group, &mut msg).await?; - - let bob_received_message = bob.recv_msg(&alice.pop_sent_msg().await).await; - assert_eq!(bob_received_message.quoted_text().unwrap(), "..."); - assert_eq!(bob_received_message.get_showpadlock(), false); - - // Alice replaces a quote of encrypted message with a quote of unencrypted one. - let mut msg1 = Message::new(Viewtype::Text); - msg1.set_quote(alice, Some(&alice_received_message)).await?; - msg1.set_quote(alice, Some(&msg)).await?; - chat::send_msg(alice, alice_group, &mut msg1).await?; - - let bob_received_message = bob.recv_msg(&alice.pop_sent_msg().await).await; - assert_eq!(bob_received_message.quoted_text().unwrap(), "unencrypted"); - assert_eq!(bob_received_message.get_showpadlock(), false); + let bob_received_message = bob.recv_msg(&sent).await; + assert_eq!(bob_received_message.get_showpadlock(), true); + let bob_chat_id = bob_received_message.chat_id; + + // Bob quotes encrypted message in unencrypted chat. + let bob_email_chat = bob.create_email_chat(alice).await; + let mut msg = Message::new_text("I am sending an unencrypted reply.".to_string()); + msg.set_quote(bob, Some(&bob_received_message)).await?; + chat::send_msg(bob, bob_email_chat.id, &mut msg).await?; + + // Alice receives unencrypted message, + // but the quote of encrypted message is replaced with "...". + let alice_received_message = alice.recv_msg(&bob.pop_sent_msg().await).await; + assert_eq!(alice_received_message.quoted_text().unwrap(), "..."); + assert_eq!(alice_received_message.get_showpadlock(), false); Ok(()) } From 24826ee2e25a66502443f062f4e4ba577f24bf93 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 22 Apr 2025 18:31:30 +0000 Subject: [PATCH 112/381] remove test_default_member_timestamps_to_zero --- src/receive_imf/receive_imf_tests.rs | 66 ---------------------------- 1 file changed, 66 deletions(-) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 6ce6502d9c..0c090fe2e5 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -5445,72 +5445,6 @@ async fn test_prefer_chat_group_id_over_references() -> Result<()> { Ok(()) } -/// Tests that if member timestamps are unknown -/// because of the missing `Chat-Group-Member-Timestamps` header, -/// then timestamps default to zero. -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_default_member_timestamps_to_zero() -> Result<()> { - let bob = &TestContext::new_bob().await; - - let now = time(); - - let date = chrono::DateTime::::from_timestamp(now - 1000, 0) - .unwrap() - .to_rfc2822(); - let msg = receive_imf( - bob, - format!( - "Subject: Some group\r\n\ - From: \r\n\ - To: , , \r\n\ - Date: {date}\r\n\ - Message-ID: \r\n\ - Chat-Group-ID: foobarbaz12\n\ - Chat-Group-Name: foo\n\ - Chat-Version: 1.0\r\n\ - \r\n\ - Hi!\r\n" - ) - .as_bytes(), - false, - ) - .await? - .unwrap(); - let chat = Chat::load_from_db(bob, msg.chat_id).await?; - assert_eq!(chat.typ, Chattype::Group); - assert_eq!(chat::get_chat_contacts(bob, chat.id).await?.len(), 4); - - let date = chrono::DateTime::::from_timestamp(now, 0) - .unwrap() - .to_rfc2822(); - receive_imf( - bob, - format!( - "Subject: Some group\r\n\ - From: \r\n\ - To: , \r\n\ - Chat-Group-Past-Members: \r\n\ - Chat-Group-Member-Timestamps: 1737783000 1737783100 1737783200\r\n\ - Chat-Group-ID: foobarbaz12\n\ - Chat-Group-Name: foo\n\ - Chat-Version: 1.0\r\n\ - Date: {date}\r\n\ - Message-ID: \r\n\ - \r\n\ - Hi back!\r\n" - ) - .as_bytes(), - false, - ) - .await? - .unwrap(); - - let chat = Chat::load_from_db(bob, msg.chat_id).await?; - assert_eq!(chat.typ, Chattype::Group); - assert_eq!(chat::get_chat_contacts(bob, chat.id).await?.len(), 3); - Ok(()) -} - /// Regression test for the bug /// that resulted in an info message /// about Bob addition to the group on Fiona's device. From 2ed83f00771b4e1b1b4b17625f3614d2cbd99a42 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 22 Apr 2025 18:37:10 +0000 Subject: [PATCH 113/381] remove receive_imf::receive_imf_tests::test_leave_protected_group_missing_member_key --- src/receive_imf/receive_imf_tests.rs | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 0c090fe2e5..d76e2a915c 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -4792,34 +4792,6 @@ Chat-Group-Member-Added: charlie@example.com", Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_leave_protected_group_missing_member_key() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = &tcm.alice().await; - let bob = &tcm.bob().await; - mark_as_verified(alice, bob).await; - let alice_bob_id = alice.add_or_lookup_contact(bob).await.id; - let group_id = create_group_chat(alice, ProtectionStatus::Protected, "Group").await?; - add_contact_to_chat(alice, group_id, alice_bob_id).await?; - alice.send_text(group_id, "Hello!").await; - alice - .sql - .execute( - "UPDATE acpeerstates SET addr=? WHERE addr=?", - ("b@b", "bob@example.net"), - ) - .await?; - - // We fail to send the message. - assert!(remove_contact_from_chat(alice, group_id, ContactId::SELF) - .await - .is_err()); - - // The contact is already removed anyway. - assert!(!is_contact_in_chat(alice, group_id, ContactId::SELF).await?); - Ok(()) -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_protected_group_add_remove_member_missing_key() -> Result<()> { let mut tcm = TestContextManager::new(); From 1ed4487b8e6d3e7c10b4de2f62a6666e99d5dd5a Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 22 Apr 2025 18:41:28 +0000 Subject: [PATCH 114/381] no acpeerstates in test_protected_group_add_remove_member_missing_key --- src/receive_imf/receive_imf_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index d76e2a915c..9944b4af72 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -4805,7 +4805,7 @@ async fn test_protected_group_add_remove_member_missing_key() -> Result<()> { alice.send_text(group_id, "Hello!").await; alice .sql - .execute("DELETE FROM acpeerstates WHERE addr=?", (&bob_addr,)) + .execute("DELETE FROM public_keys", ()) .await?; let fiona = &tcm.fiona().await; From e481198649566e9e41fac6141db4fc941f917080 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 22 Apr 2025 19:03:58 +0000 Subject: [PATCH 115/381] fix securejoin::securejoin_tests::test_setup_contact_bob_knows_alice --- src/securejoin/securejoin_tests.rs | 54 +++++++++++++----------------- 1 file changed, 23 insertions(+), 31 deletions(-) diff --git a/src/securejoin/securejoin_tests.rs b/src/securejoin/securejoin_tests.rs index 3bc0caf950..4d090747de 100644 --- a/src/securejoin/securejoin_tests.rs +++ b/src/securejoin/securejoin_tests.rs @@ -346,16 +346,17 @@ async fn test_setup_contact_bad_qr() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_setup_contact_bob_knows_alice() -> Result<()> { let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; // Ensure Bob knows Alice_FP - let alice_contact_id = bob.add_or_lookup_contact_id(&alice).await; + let alice_contact_id = bob.add_or_lookup_contact_id(alice).await; - // Step 1: Generate QR-code, ChatId(0) indicates setup-contact + tcm.section("Step 1: Generate QR-code"); + // `None` indicates setup-contact. let qr = get_securejoin_qr(&alice.ctx, None).await?; - // Step 2+4: Bob scans QR-code, sends vc-request-with-auth, skipping vc-request + tcm.section("Step 2+4: Bob scans QR-code, sends vc-request-with-auth, skipping vc-request"); join_securejoin(&bob.ctx, &qr).await.unwrap(); // Check Bob emitted the JoinerProgress event. @@ -383,24 +384,17 @@ async fn test_setup_contact_bob_knows_alice() -> Result<()> { "vc-request-with-auth" ); assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some()); - let bob_fp = load_self_public_key(&bob.ctx).await?.dc_fingerprint(); + let bob_fp = load_self_public_key(bob).await?.dc_fingerprint(); assert_eq!( *msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(), bob_fp.hex() ); // Alice should not yet have Bob verified - let (contact_bob_id, _modified) = Contact::add_or_lookup( - &alice.ctx, - "", - &ContactAddress::new("bob@example.net")?, - Origin::ManuallyCreated, - ) - .await?; - let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id).await?; + let contact_bob = alice.add_or_lookup_pgp_contact(bob).await; assert_eq!(contact_bob.is_verified(&alice.ctx).await?, false); - // Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm + tcm.section("Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm"); alice.recv_msg_trash(&sent).await; assert_eq!(contact_bob.is_verified(&alice.ctx).await?, true); @@ -412,18 +406,16 @@ async fn test_setup_contact_bob_knows_alice() -> Result<()> { "vc-contact-confirm" ); - // Bob should not yet have Alice verified - let contact_alice_id = - Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown) - .await - .expect("Error looking up contact") - .expect("Contact not found"); - let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id).await?; - assert_eq!(contact_alice.is_verified(&bob.ctx).await?, false); + // Bob has verified Alice already. + let contact_alice = bob.add_or_lookup_pgp_contact(alice).await; + assert_eq!(contact_alice.is_verified(bob).await?, true); - // Step 7: Bob receives vc-contact-confirm + // Alice confirms that Bob is now verified. + // + // This does not change anything for Bob. + tcm.section("Step 7: Bob receives vc-contact-confirm"); bob.recv_msg_trash(&sent).await; - assert_eq!(contact_alice.is_verified(&bob.ctx).await?, true); + assert_eq!(contact_alice.is_verified(bob).await?, true); Ok(()) } @@ -470,15 +462,15 @@ async fn test_secure_join() -> Result<()> { assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0); let alice_chatid = - chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat").await?; + chat::create_group_chat(&alice, ProtectionStatus::Protected, "the chat").await?; tcm.section("Step 1: Generate QR-code, secure-join implied by chatid"); - let qr = get_securejoin_qr(&alice.ctx, Some(alice_chatid)) + let qr = get_securejoin_qr(&alice, Some(alice_chatid)) .await .unwrap(); tcm.section("Step 2: Bob scans QR-code, sends vg-request"); - let bob_chatid = join_securejoin(&bob.ctx, &qr).await?; + let bob_chatid = join_securejoin(&bob, &qr).await?; assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1); let sent = bob.pop_sent_msg().await; @@ -543,7 +535,7 @@ async fn test_secure_join() -> Result<()> { "vg-request-with-auth" ); assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some()); - let bob_fp = load_self_public_key(&bob.ctx).await?.dc_fingerprint(); + let bob_fp = load_self_public_key(&bob).await?.dc_fingerprint(); assert_eq!( *msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(), bob_fp.hex() @@ -594,13 +586,13 @@ async fn test_secure_join() -> Result<()> { // // Alice may not have verified Bob yet. let contact_alice = bob.add_or_lookup_pgp_contact(&alice).await; - assert_eq!(contact_alice.is_verified(&bob.ctx).await?, true); + assert_eq!(contact_alice.is_verified(&bob).await?, true); tcm.section("Step 7: Bob receives vg-member-added"); bob.recv_msg(&sent).await; { // Bob has Alice verified, message shows up in the group chat. - assert_eq!(contact_alice.is_verified(&bob.ctx).await?, true); + assert_eq!(contact_alice.is_verified(&bob).await?, true); let chat = bob.get_pgp_chat(&alice).await; assert_eq!( chat.blocked, From 438bf3cb8ed237cf104ea69a75d8483a2402c13f Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 22 Apr 2025 19:19:43 +0000 Subject: [PATCH 116/381] fix securejoin::securejoin_tests::test_setup_contact_wrong_alice_gossip --- src/securejoin/securejoin_tests.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/securejoin/securejoin_tests.rs b/src/securejoin/securejoin_tests.rs index 4d090747de..c969d94dcc 100644 --- a/src/securejoin/securejoin_tests.rs +++ b/src/securejoin/securejoin_tests.rs @@ -97,7 +97,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) { ); tcm.section("Step 1: Generate QR-code, ChatId(0) indicates setup-contact"); - let qr = get_securejoin_qr(&alice.ctx, None).await.unwrap(); + let qr = get_securejoin_qr(&alice, None).await.unwrap(); // We want Bob to learn Alice's name from their messages, not from the QR code. alice .set_config(Config::Displayname, Some("Alice Exampleorg")) @@ -202,12 +202,12 @@ async fn test_setup_contact_ex(case: SetupContactCase) { .gossiped_keys .insert(alice_addr.to_string(), wrong_pubkey) .unwrap(); - let contact_bob = alice.add_or_lookup_email_contact(&bob).await; + let contact_bob = alice.add_or_lookup_pgp_contact(&bob).await; let handshake_msg = handle_securejoin_handshake(&alice, &mut msg, contact_bob.id) .await .unwrap(); assert_eq!(handshake_msg, HandshakeMessage::Ignore); - assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), false); + assert_eq!(contact_bob.is_verified(&alice).await.unwrap(), false); msg.gossiped_keys .insert(alice_addr.to_string(), alice_pubkey) @@ -216,7 +216,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) { .await .unwrap(); assert_eq!(handshake_msg, HandshakeMessage::Ignore); - assert!(contact_bob.is_verified(&alice.ctx).await.unwrap()); + assert!(contact_bob.is_verified(&alice).await.unwrap()); return; } @@ -224,7 +224,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) { let contact_bob = alice.add_or_lookup_pgp_contact(&bob).await; let contact_bob_id = contact_bob.id; assert_eq!(contact_bob.is_pgp_contact(), true); - assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), false); + assert_eq!(contact_bob.is_verified(&alice).await.unwrap(), false); assert_eq!(contact_bob.get_authname(), ""); if case == SetupContactCase::CheckProtectionTimestamp { @@ -233,8 +233,8 @@ async fn test_setup_contact_ex(case: SetupContactCase) { tcm.section("Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm"); alice.recv_msg_trash(&sent).await; - assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), true); - let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id) + assert_eq!(contact_bob.is_verified(&alice).await.unwrap(), true); + let contact_bob = Contact::get_by_id(&alice, contact_bob_id) .await .unwrap(); assert_eq!(contact_bob.get_authname(), "Bob Examplenet"); @@ -354,10 +354,10 @@ async fn test_setup_contact_bob_knows_alice() -> Result<()> { tcm.section("Step 1: Generate QR-code"); // `None` indicates setup-contact. - let qr = get_securejoin_qr(&alice.ctx, None).await?; + let qr = get_securejoin_qr(&alice, None).await?; tcm.section("Step 2+4: Bob scans QR-code, sends vc-request-with-auth, skipping vc-request"); - join_securejoin(&bob.ctx, &qr).await.unwrap(); + join_securejoin(&bob, &qr).await.unwrap(); // Check Bob emitted the JoinerProgress event. let event = bob @@ -392,11 +392,11 @@ async fn test_setup_contact_bob_knows_alice() -> Result<()> { // Alice should not yet have Bob verified let contact_bob = alice.add_or_lookup_pgp_contact(bob).await; - assert_eq!(contact_bob.is_verified(&alice.ctx).await?, false); + assert_eq!(contact_bob.is_verified(&alice).await?, false); tcm.section("Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm"); alice.recv_msg_trash(&sent).await; - assert_eq!(contact_bob.is_verified(&alice.ctx).await?, true); + assert_eq!(contact_bob.is_verified(&alice).await?, true); let sent = alice.pop_sent_msg().await; let msg = bob.parse_msg(&sent).await; @@ -543,11 +543,11 @@ async fn test_secure_join() -> Result<()> { // Alice should not yet have Bob verified let contact_bob = alice.add_or_lookup_pgp_contact(&bob).await; - assert_eq!(contact_bob.is_verified(&alice.ctx).await?, false); + assert_eq!(contact_bob.is_verified(&alice).await?, false); tcm.section("Step 5+6: Alice receives vg-request-with-auth, sends vg-member-added"); alice.recv_msg_trash(&sent).await; - assert_eq!(contact_bob.is_verified(&alice.ctx).await?, true); + assert_eq!(contact_bob.is_verified(&alice).await?, true); let sent = alice.pop_sent_msg().await; let msg = bob.parse_msg(&sent).await; @@ -693,7 +693,7 @@ async fn test_lost_contact_confirm() { .unwrap(); } - let qr = get_securejoin_qr(&alice.ctx, None).await.unwrap(); + let qr = get_securejoin_qr(&alice, None).await.unwrap(); join_securejoin(&bob.ctx, &qr).await.unwrap(); // vc-request @@ -710,7 +710,7 @@ async fn test_lost_contact_confirm() { // Alice has Bob verified now. let contact_bob = alice.add_or_lookup_pgp_contact(&bob).await; - assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), true); + assert_eq!(contact_bob.is_verified(&alice).await.unwrap(), true); // Alice sends vc-contact-confirm, but it gets lost. let _sent_vc_contact_confirm = alice.pop_sent_msg().await; From 939d2651848ccd80765b66f8be6a2a1ff96d21ad Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 22 Apr 2025 19:25:02 +0000 Subject: [PATCH 117/381] remove receive_imf::receive_imf_tests::test_reply_from_different_addr --- src/receive_imf/receive_imf_tests.rs | 64 ---------------------------- 1 file changed, 64 deletions(-) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 9944b4af72..3c491ddf32 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -3148,70 +3148,6 @@ async fn test_invalid_to_address() -> Result<()> { Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_reply_from_different_addr() -> Result<()> { - let t = TestContext::new_alice().await; - - // Alice creates a 2-person-group with Bob - receive_imf( - &t, - br#"Subject: =?utf-8?q?Januar_13-19?= -Chat-Group-ID: qetqsutor7a -Chat-Group-Name: =?utf-8?q?Januar_13-19?= -MIME-Version: 1.0 -References: -Date: Mon, 20 Dec 2021 12:15:01 +0000 -Chat-Version: 1.0 -Message-ID: -To: -From: -Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no - -Hi, I created a group"#, - false, - ) - .await?; - let msg_out = t.get_last_msg().await; - assert_eq!(msg_out.from_id, ContactId::SELF); - assert_eq!(msg_out.text, "Hi, I created a group"); - assert_eq!(msg_out.in_reply_to, None); - - // Bob replies from a different address - receive_imf( - &t, - b"Content-Type: text/plain; charset=utf-8 -Content-Transfer-Encoding: quoted-printable -From: -Mime-Version: 1.0 (1.0) -Subject: Re: Januar 13-19 -Date: Mon, 20 Dec 2021 13:54:55 +0100 -Message-Id: -References: -In-Reply-To: -To: holger - -Reply from different address -", - false, - ) - .await?; - let msg_in = t.get_last_msg().await; - assert_eq!(msg_in.to_id, ContactId::SELF); - assert_eq!(msg_in.text, "Reply from different address"); - assert_eq!( - msg_in.in_reply_to.unwrap(), - "Gr.qetqsutor7a.Aresxresy-4@deltachat.de" - ); - assert_eq!( - msg_in.param.get(Param::OverrideSenderDisplayname), - Some("bob-alias@example.com") - ); - - assert_eq!(msg_in.chat_id, msg_out.chat_id); - - Ok(()) -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_weird_and_duplicated_filenames() -> Result<()> { let mut tcm = TestContextManager::new(); From 6714844ec8cef7aa806f599610ab56da263ebf99 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 22 Apr 2025 19:32:32 +0000 Subject: [PATCH 118/381] simplify test_rfc1847_encapsulation (still broken) --- src/receive_imf/receive_imf_tests.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 3c491ddf32..696b8eff97 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -3115,20 +3115,16 @@ Message with references."#; /// Test a message with RFC 1847 encapsulation as created by Thunderbird. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_rfc1847_encapsulation() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; - // Alice sends an Autocrypt message to Bob so Bob gets Alice's key. - let chat_alice = alice.create_chat(&bob).await; - let first_msg = alice - .send_text(chat_alice.id, "Sending Alice key to Bob.") - .await; - bob.recv_msg(&first_msg).await; - message::delete_msgs(&bob, &[bob.get_last_msg().await.id]).await?; + // Bob gets Alice's key via vCard. + bob.add_or_lookup_contact_id(alice).await; // Alice sends a message to Bob using Thunderbird. let raw = include_bytes!("../../test-data/message/rfc1847_encapsulation.eml"); - receive_imf(&bob, raw, false).await?; + receive_imf(bob, raw, false).await?; let msg = bob.get_last_msg().await; assert!(msg.get_showpadlock()); From bd9f000a7d0187c29361c7019d9f04869105cb46 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 22 Apr 2025 19:36:26 +0000 Subject: [PATCH 119/381] remove securejoin::securejoin_tests::test_shared_bobs_key --- src/securejoin/securejoin_tests.rs | 56 ------------------------------ 1 file changed, 56 deletions(-) diff --git a/src/securejoin/securejoin_tests.rs b/src/securejoin/securejoin_tests.rs index c969d94dcc..140afb0384 100644 --- a/src/securejoin/securejoin_tests.rs +++ b/src/securejoin/securejoin_tests.rs @@ -720,62 +720,6 @@ async fn test_lost_contact_confirm() { assert_eq!(contact_alice.is_verified(&bob).await.unwrap(), true); } -/// An unencrypted message with already known Autocrypt key, but sent from another address, -/// means that it's rather a new contact sharing the same key than the existing one changed its -/// address, otherwise it would already have our key to encrypt. -/// -/// This is a regression test for a bug where DC wrongly executed AEAP in this case. -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_shared_bobs_key() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = &tcm.alice().await; - let bob = &tcm.bob().await; - let bob_addr = &bob.get_config(Config::Addr).await?.unwrap(); - - tcm.execute_securejoin(bob, alice).await; - - let export_dir = tempfile::tempdir().unwrap(); - imex(bob, ImexMode::ExportSelfKeys, export_dir.path(), None).await?; - let bob2 = &TestContext::new().await; - let bob2_addr = "bob2@example.net"; - bob2.configure_addr(bob2_addr).await; - imex(bob2, ImexMode::ImportSelfKeys, export_dir.path(), None).await?; - - tcm.execute_securejoin(bob2, alice).await; - - let bob3 = &TestContext::new().await; - let bob3_addr = "bob3@example.net"; - bob3.configure_addr(bob3_addr).await; - imex(bob3, ImexMode::ImportSelfKeys, export_dir.path(), None).await?; - let chat = bob3.create_email_chat(alice).await; - let sent = bob3.send_text(chat.id, "hi Alice!").await; - let msg = alice.recv_msg(&sent).await; - assert!(!msg.get_showpadlock()); - let chat = alice.create_email_chat(bob3).await; - let sent = alice.send_text(chat.id, "hi Bob3!").await; - let msg = bob3.recv_msg(&sent).await; - assert!(msg.get_showpadlock()); - - let mut bob_ids = HashSet::new(); - bob_ids.insert( - Contact::lookup_id_by_addr(alice, bob_addr, Origin::Unknown) - .await? - .unwrap(), - ); - bob_ids.insert( - Contact::lookup_id_by_addr(alice, bob2_addr, Origin::Unknown) - .await? - .unwrap(), - ); - bob_ids.insert( - Contact::lookup_id_by_addr(alice, bob3_addr, Origin::Unknown) - .await? - .unwrap(), - ); - assert_eq!(bob_ids.len(), 3); - Ok(()) -} - /// Tests Bob joining two groups by scanning two QR codes /// from the same Alice at the same time. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] From 82493df619538b95d36404257ff31fe5a16618d9 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 22 Apr 2025 20:41:05 +0000 Subject: [PATCH 120/381] fix test_write_to_alice_after_aeap --- src/tests/aeap.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/tests/aeap.rs b/src/tests/aeap.rs index de6e54bf1c..a26b89f305 100644 --- a/src/tests/aeap.rs +++ b/src/tests/aeap.rs @@ -245,11 +245,17 @@ async fn test_aeap_replay_attack() -> Result<()> { Ok(()) } +/// Tests that writing to a contact is possible +/// after address change. +/// +/// This test is redundant after introduction +/// of PGP-contacts, but is kept to avoid deleting the tests. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_write_to_alice_after_aeap() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = &tcm.alice().await; let bob = &tcm.bob().await; + let alice_grp_id = chat::create_group_chat(alice, ProtectionStatus::Protected, "Group").await?; let qr = get_securejoin_qr(alice, Some(alice_grp_id)).await?; tcm.exec_securejoin_qr(bob, alice, &qr).await; @@ -265,15 +271,13 @@ async fn test_write_to_alice_after_aeap() -> Result<()> { let sent = alice.send_text(alice_grp_id, "Hello!").await; bob.recv_msg(&sent).await; - assert!(!bob_alice_contact.is_verified(bob).await?); + assert!(bob_alice_contact.is_verified(bob).await?); let bob_alice_chat = Chat::load_from_db(bob, bob_alice_chat.id).await?; assert!(bob_alice_chat.is_protected()); let mut msg = Message::new_text("hi".to_string()); - assert!(chat::send_msg(bob, bob_alice_chat.id, &mut msg) - .await - .is_err()); + chat::send_msg(bob, bob_alice_chat.id, &mut msg).await?; - // But encrypted communication is still possible in unprotected groups with old Alice. + // Encrypted communication is also possible in unprotected groups with Alice. let sent = bob .send_text(bob_unprotected_grp_id, "Alice, how is your address change?") .await; From f00f77ee84034dc53a5aceee56fb48d2d7e07834 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 23 Apr 2025 15:27:28 +0000 Subject: [PATCH 121/381] fix test_message_from_old_dc_setup --- src/tests/verified_chats.rs | 20 ++++++++----------- .../verified_chats_message_from_old_dc_setup | 8 -------- 2 files changed, 8 insertions(+), 20 deletions(-) delete mode 100644 test-data/golden/verified_chats_message_from_old_dc_setup diff --git a/src/tests/verified_chats.rs b/src/tests/verified_chats.rs index 7dd48941c1..b4f3a8a118 100644 --- a/src/tests/verified_chats.rs +++ b/src/tests/verified_chats.rs @@ -600,14 +600,17 @@ async fn test_reply() -> Result<()> { Ok(()) } +/// Tests that message from old DC setup does not break +/// new verified chat. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_message_from_old_dc_setup() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = &tcm.alice().await; let bob_old = &tcm.unconfigured().await; + enable_verified_oneonone_chats(&[alice, bob_old]).await; - mark_as_verified(bob_old, alice).await; bob_old.configure_addr("bob@example.net").await; + mark_as_verified(bob_old, alice).await; let chat = bob_old.create_chat(alice).await; let sent_old = bob_old .send_text(chat.id, "Soon i'll have a new device") @@ -625,22 +628,15 @@ async fn test_message_from_old_dc_setup() -> Result<()> { assert_verified(alice, bob, ProtectionStatus::Protected).await; let msg = alice.recv_msg(&sent_old).await; - assert!(!msg.get_showpadlock()); + assert!(msg.get_showpadlock()); let contact = alice.add_or_lookup_contact(bob).await; - // The outdated Bob's Autocrypt header isn't applied, so the verification preserves. + + // The outdated Bob's Autocrypt header isn't applied + // and the message goes to another chat, so the verification preserves. assert!(contact.is_verified(alice).await.unwrap()); let chat = alice.get_pgp_chat(bob).await; assert!(chat.is_protected()); assert_eq!(chat.is_protection_broken(), false); - let protection_msg = alice.get_last_msg().await; - assert_eq!( - protection_msg.param.get_cmd(), - SystemMessage::ChatProtectionEnabled - ); - assert!(protection_msg.timestamp_sort >= msg.timestamp_rcvd); - alice - .golden_test_chat(msg.chat_id, "verified_chats_message_from_old_dc_setup") - .await; Ok(()) } diff --git a/test-data/golden/verified_chats_message_from_old_dc_setup b/test-data/golden/verified_chats_message_from_old_dc_setup deleted file mode 100644 index b319750902..0000000000 --- a/test-data/golden/verified_chats_message_from_old_dc_setup +++ /dev/null @@ -1,8 +0,0 @@ -Single#Chat#10: bob@example.net [PGP bob@example.net] 🛡️ --------------------------------------------------------------------------------- -Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️] -Msg#11🔒: (Contact#Contact#10): Now i have it! [FRESH] -Msg#12: info (Contact#Contact#Info): bob@example.net sent a message from another device. [NOTICED][INFO 🛡️❌] -Msg#13: (Contact#Contact#10): Soon i'll have a new device [FRESH] -Msg#14: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️] --------------------------------------------------------------------------------- From 6a239f6feacf0c535a169e3b317bdd7987cb93fe Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 23 Apr 2025 15:43:24 +0000 Subject: [PATCH 122/381] remove test_old_message tests --- src/tests/verified_chats.rs | 131 ------------------------------------ 1 file changed, 131 deletions(-) diff --git a/src/tests/verified_chats.rs b/src/tests/verified_chats.rs index b4f3a8a118..72a8fd8d84 100644 --- a/src/tests/verified_chats.rs +++ b/src/tests/verified_chats.rs @@ -273,137 +273,6 @@ async fn test_degrade_verified_oneonone_chat() -> Result<()> { Ok(()) } -/// Messages with old timestamps are difficult for verified chats: -/// - They must not be sorted over a protection-changed info message. -/// That's what `test_old_message_2` tests -/// - If they change the protection, then they must not be sorted over existing other messages, -/// because then the protection-changed info message would also be above these existing messages. -/// That's what `test_old_message_3` tests. -/// -/// `test_old_message_1` tests the case where both the old and the new message -/// change verification -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_old_message_1() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - enable_verified_oneonone_chats(&[&alice, &bob]).await; - - mark_as_verified(&alice, &bob).await; - - let chat = alice.create_chat(&bob).await; // This creates a protection-changed info message - assert!(chat.is_protected()); - - // This creates protection-changed info message #2; - // even though the date is old, info message and email must be sorted below the original info message. - receive_imf( - &alice, - b"From: Bob \n\ - To: alice@example.org\n\ - Message-ID: <1234-2-3@example.org>\n\ - Date: Sat, 07 Dec 2019 19:00:27 +0000\n\ - \n\ - Message from Thunderbird\n", - true, - ) - .await?; - - alice.golden_test_chat(chat.id, "test_old_message_1").await; - - Ok(()) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_old_message_2() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - enable_verified_oneonone_chats(&[&alice, &bob]).await; - - mark_as_verified(&alice, &bob).await; - - // This creates protection-changed info message #1: - let chat = alice.create_chat(&bob).await; - assert!(chat.is_protected()); - let protection_msg = alice.get_last_msg().await; - assert_eq!( - protection_msg.param.get_cmd(), - SystemMessage::ChatProtectionEnabled - ); - - // This creates protection-changed info message #2 with `timestamp_sort` greater by 1. - let first_email = receive_imf( - &alice, - b"From: Bob \n\ - To: alice@example.org\n\ - Message-ID: <1234-2-3@example.org>\n\ - Date: Sun, 08 Dec 2019 19:00:27 +0000\n\ - \n\ - Somewhat old message\n", - false, - ) - .await? - .unwrap(); - - // Both messages will get the same timestamp, so this one will be sorted under the previous one - // even though it has an older timestamp. - let second_email = receive_imf( - &alice, - b"From: Bob \n\ - To: alice@example.org\n\ - Message-ID: <2319-2-3@example.org>\n\ - Date: Sat, 07 Dec 2019 19:00:27 +0000\n\ - \n\ - Even older message, that must NOT be shown before the info message\n", - true, - ) - .await? - .unwrap(); - - assert_eq!(first_email.sort_timestamp, second_email.sort_timestamp); - assert_eq!( - first_email.sort_timestamp, - protection_msg.timestamp_sort + 1 - ); - - alice.golden_test_chat(chat.id, "test_old_message_2").await; - - Ok(()) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_old_message_3() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - enable_verified_oneonone_chats(&[&alice, &bob]).await; - - mark_as_verified(&alice, &bob).await; - mark_as_verified(&bob, &alice).await; - - tcm.send_recv_accept(&bob, &alice, "Heyho from my verified device!") - .await; - - // This unverified message must not be sorted over the message sent in the previous line: - receive_imf( - &alice, - b"From: Bob \n\ - To: alice@example.org\n\ - Message-ID: <1234-2-3@example.org>\n\ - Date: Sat, 07 Dec 2019 19:00:27 +0000\n\ - \n\ - Old, unverified message\n", - true, - ) - .await?; - - alice - .golden_test_chat(alice.get_pgp_chat(&bob).await.id, "test_old_message_3") - .await; - - Ok(()) -} - /// Alice is offline for some time. /// When she comes online, first her inbox is synced and then her sentbox. /// This test tests that the messages are still in the right order. From 10ebfe0af0f48ab9d9a8d0a1814dd66f78291eb3 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 23 Apr 2025 17:20:24 +0000 Subject: [PATCH 123/381] fixing rpc client tests --- deltachat-rpc-client/tests/test_securejoin.py | 37 +++++++++---------- deltachat-rpc-client/tests/test_something.py | 2 +- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/deltachat-rpc-client/tests/test_securejoin.py b/deltachat-rpc-client/tests/test_securejoin.py index 03ade4d04e..01f10ba325 100644 --- a/deltachat-rpc-client/tests/test_securejoin.py +++ b/deltachat-rpc-client/tests/test_securejoin.py @@ -16,14 +16,14 @@ def test_qr_setup_contact(acfactory, tmp_path) -> None: alice.wait_for_securejoin_inviter_success() # Test that Alice verified Bob's profile. - alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr")) + alice_contact_bob = alice.create_contact(bob) alice_contact_bob_snapshot = alice_contact_bob.get_snapshot() assert alice_contact_bob_snapshot.is_verified bob.wait_for_securejoin_joiner_success() # Test that Bob verified Alice's profile. - bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr")) + bob_contact_alice = bob.create_contact(bob) bob_contact_alice_snapshot = bob_contact_alice.get_snapshot() assert bob_contact_alice_snapshot.is_verified @@ -84,7 +84,7 @@ def test_qr_securejoin(acfactory, protect): bob.wait_for_securejoin_joiner_success() # Test that Alice verified Bob's profile. - alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr")) + alice_contact_bob = alice.create_contact(bob) alice_contact_bob_snapshot = alice_contact_bob.get_snapshot() assert alice_contact_bob_snapshot.is_verified @@ -93,7 +93,7 @@ def test_qr_securejoin(acfactory, protect): assert snapshot.chat.get_basic_snapshot().is_protected == protect # Test that Bob verified Alice's profile. - bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr")) + bob_contact_alice = bob.create_contact(alice) bob_contact_alice_snapshot = bob_contact_alice.get_snapshot() assert bob_contact_alice_snapshot.is_verified @@ -101,7 +101,7 @@ def test_qr_securejoin(acfactory, protect): # Alice observes securejoin protocol and verifies Bob on second device. alice2.start_io() alice2.wait_for_securejoin_inviter_success() - alice2_contact_bob = alice2.get_contact_by_addr(bob.get_config("addr")) + alice2_contact_bob = alice2.create_contact(bob) alice2_contact_bob_snapshot = alice2_contact_bob.get_snapshot() assert alice2_contact_bob_snapshot.is_verified @@ -227,7 +227,7 @@ def test_verified_group_recovery(acfactory) -> None: ac2.wait_for_securejoin_joiner_success() # ac1 has ac2 directly verified. - ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr")) + ac1_contact_ac2 = ac1.create_contact(ac2) assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF logging.info("ac3 joins verified group") @@ -278,7 +278,7 @@ def test_verified_group_recovery(acfactory) -> None: def test_verified_group_member_added_recovery(acfactory) -> None: - """Tests verified group recovery by reverifiying than removing and adding a member back.""" + """Tests verified group recovery by reverifying than removing and adding a member back.""" ac1, ac2, ac3 = acfactory.get_online_accounts(3) logging.info("ac1 creates verified group") @@ -291,7 +291,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None: ac2.wait_for_securejoin_joiner_success() # ac1 has ac2 directly verified. - ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr")) + ac1_contact_ac2 = ac1.create_contact(ac2) assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF logging.info("ac3 joins verified group") @@ -319,7 +319,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None: ac1.wait_for_incoming_msg_event() # Hi! - ac3_contact_ac2 = ac3.get_contact_by_addr(ac2.get_config("addr")) + ac3_contact_ac2 = ac3.create_contact(ac2) ac3_chat.remove_contact(ac3_contact_ac2) msg_id = ac2.wait_for_incoming_msg_event().msg_id @@ -354,19 +354,16 @@ def test_verified_group_member_added_recovery(acfactory) -> None: snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot() assert snapshot.text == "Works again!" - ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr")) + ac1_contact_ac2 = ac1.create_contact(ac2) + ac1_contact_ac3 = ac1.create_contact(ac3) ac1_contact_ac2_snapshot = ac1_contact_ac2.get_snapshot() assert ac1_contact_ac2_snapshot.is_verified - assert ac1_contact_ac2_snapshot.verifier_id == ac1.get_contact_by_addr(ac3.get_config("addr")).id - - # ac2 is now verified by ac3 for ac1 - ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr")) - assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id + assert ac1_contact_ac2_snapshot.verifier_id == ac1_contact_ac3.id def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory): """Regression test for - issue . + issue . """ ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4) @@ -400,12 +397,12 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory): logging.info("ac2 now has pending bobstate but ac1 is shutoff") # we meanwhile expect ac3/ac2 verification started in the beginning to have completed - assert ac3.get_contact_by_addr(ac2.get_config("addr")).get_snapshot().is_verified - assert ac2.get_contact_by_addr(ac3.get_config("addr")).get_snapshot().is_verified + assert ac3.create_contact(ac2).get_snapshot().is_verified + assert ac2.create_contact(ac3).get_snapshot().is_verified logging.info("ac3: create a verified group VG with ac2") vg = ac3.create_group("ac3-created", protect=True) - vg.add_contact(ac3.get_contact_by_addr(ac2.get_config("addr"))) + vg.add_contact(ac3.create_contact(ac2)) # ensure ac2 receives message in VG vg.send_text("hello") @@ -443,7 +440,7 @@ def test_qr_new_group_unblocked(acfactory): ac1.wait_for_securejoin_inviter_success() ac1_new_chat = ac1.create_group("Another group") - ac1_new_chat.add_contact(ac1.get_contact_by_addr(ac2.get_config("addr"))) + ac1_new_chat.add_contact(ac1.create_contact(ac2)) # Receive "Member added" message. ac2.wait_for_incoming_msg_event() diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index 93d96fdd2e..71f13a127b 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -170,7 +170,7 @@ def test_account(acfactory) -> None: assert alice.get_size() assert alice.is_configured() assert not alice.get_avatar() - assert alice.get_contact_by_addr(bob_addr) == alice_contact_bob + assert alice.get_contact_by_addr(bob_addr) == None # There is no email-contact, only PGP-contact assert alice.get_contacts() assert alice.get_contacts(snapshot=True) assert alice.self_contact From c2eedd0dd370ab24f4031e9050a2348c9627295d Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 23 Apr 2025 18:23:51 +0000 Subject: [PATCH 124/381] fix verification by self --- src/contact.rs | 17 ++++++++++------- src/context.rs | 2 +- src/securejoin.rs | 4 ++-- src/test_utils.rs | 2 +- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/contact.rs b/src/contact.rs index 97729fac7a..a5165437c2 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -1867,6 +1867,7 @@ pub(crate) async fn mark_contact_id_as_verified( contact_id: ContactId, verifier_id: ContactId, ) -> Result<()> { + debug_assert_ne!(contact_id, verifier_id, "Contact cannot be verified by self"); context .sql .transaction(|transaction| { @@ -1878,13 +1879,15 @@ pub(crate) async fn mark_contact_id_as_verified( if contact_fingerprint.is_empty() { bail!("Non-PGP contact {contact_id} cannot be verified."); } - let verifier_fingerprint: String = transaction.query_row( - "SELECT fingerprint FROM contacts WHERE id=?", - (verifier_id,), - |row| row.get(0), - )?; - if verifier_fingerprint.is_empty() { - bail!("Contact {contact_id} cannot be verified by non-PGP contact {verifier_id}."); + if verifier_id != ContactId::SELF { + let verifier_fingerprint: String = transaction.query_row( + "SELECT fingerprint FROM contacts WHERE id=?", + (verifier_id,), + |row| row.get(0), + )?; + if verifier_fingerprint.is_empty() { + bail!("Contact {contact_id} cannot be verified by non-PGP contact {verifier_id}."); + } } transaction.execute( "UPDATE contacts SET verifier=? WHERE id=?", diff --git a/src/context.rs b/src/context.rs index 7347f52b46..05fd1e43a5 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1168,7 +1168,7 @@ impl Context { .await? .first() .context("Self reporting bot vCard does not contain a contact")?; - mark_contact_id_as_verified(self, contact_id, contact_id).await?; + mark_contact_id_as_verified(self, contact_id, ContactId::SELF).await?; let chat_id = ChatId::create_for_contact(self, contact_id).await?; chat_id diff --git a/src/securejoin.rs b/src/securejoin.rs index da4dfc4066..e747a00d66 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -208,7 +208,7 @@ async fn verify_sender_by_fingerprint( let contact = Contact::get_by_id(context, contact_id).await?; let is_verified = contact.fingerprint().is_some_and(|fp| &fp == fingerprint); if is_verified { - mark_contact_id_as_verified(context, contact_id, contact_id).await?; + mark_contact_id_as_verified(context, contact_id, ContactId::SELF).await?; } Ok(is_verified) } @@ -537,7 +537,7 @@ pub(crate) async fn observe_securejoin_on_other_device( return Ok(HandshakeMessage::Ignore); }; - mark_contact_id_as_verified(context, contact_id, contact_id).await?; + mark_contact_id_as_verified(context, contact_id, ContactId::SELF).await?; ChatId::set_protection_for_contact(context, contact_id, mime_message.timestamp_sent).await?; diff --git a/src/test_utils.rs b/src/test_utils.rs index 3813e36764..98af9c084d 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -1370,7 +1370,7 @@ fn print_logevent(logevent: &LogEvent) { /// and peerstate as backwards verified. pub(crate) async fn mark_as_verified(this: &TestContext, other: &TestContext) { let contact_id = this.add_or_lookup_contact_id(other).await; - mark_contact_id_as_verified(this, contact_id, contact_id) + mark_contact_id_as_verified(this, contact_id, ContactId::SELF) .await .unwrap(); } From 9d24d63409eee64ba4c8622370c61c4d39fb040b Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 23 Apr 2025 20:54:15 +0000 Subject: [PATCH 125/381] fix broadcast list synchronization --- src/chat.rs | 104 +++++++++++++++++++++++---- src/chat/chat_tests.rs | 15 ++-- src/contact.rs | 9 ++- src/message/message_tests.rs | 9 ++- src/receive_imf/receive_imf_tests.rs | 5 +- src/securejoin/securejoin_tests.rs | 12 +--- src/tests/aeap.rs | 4 +- src/tests/verified_chats.rs | 3 +- 8 files changed, 115 insertions(+), 46 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index e8c46ed368..e1c4e2c71a 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -2287,19 +2287,40 @@ impl Chat { /// Sends a `SyncAction` synchronising chat contacts to other devices. pub(crate) async fn sync_contacts(&self, context: &Context) -> Result<()> { - let addrs = context - .sql - .query_map( - "SELECT c.addr \ - FROM contacts c INNER JOIN chats_contacts cc \ - ON c.id=cc.contact_id \ - WHERE cc.chat_id=? AND cc.add_timestamp >= cc.remove_timestamp", - (self.id,), - |row| row.get::<_, String>(0), - |addrs| addrs.collect::, _>>().map_err(Into::into), - ) - .await?; - self.sync(context, SyncAction::SetContacts(addrs)).await + if self.is_encrypted(context).await? { + let fingerprint_addrs = context + .sql + .query_map( + "SELECT c.fingerprint, c.addr + FROM contacts c INNER JOIN chats_contacts cc + ON c.id=cc.contact_id + WHERE cc.chat_id=? AND cc.add_timestamp >= cc.remove_timestamp", + (self.id,), + |row| { + let fingerprint = row.get(0)?; + let addr = row.get(1)?; + Ok((fingerprint, addr)) + }, + |addrs| addrs.collect::, _>>().map_err(Into::into), + ) + .await?; + self.sync(context, SyncAction::SetPgpContacts(fingerprint_addrs)).await?; + } else { + let addrs = context + .sql + .query_map( + "SELECT c.addr \ + FROM contacts c INNER JOIN chats_contacts cc \ + ON c.id=cc.contact_id \ + WHERE cc.chat_id=? AND cc.add_timestamp >= cc.remove_timestamp", + (self.id,), + |row| row.get::<_, String>(0), + |addrs| addrs.collect::, _>>().map_err(Into::into), + ) + .await?; + self.sync(context, SyncAction::SetContacts(addrs)).await?; + } + Ok(()) } /// Returns chat id for the purpose of synchronisation across devices. @@ -4811,6 +4832,10 @@ pub(crate) async fn update_msg_text_and_timestamp( /// Set chat contacts by their addresses creating the corresponding contacts if necessary. async fn set_contacts_by_addrs(context: &Context, id: ChatId, addrs: &[String]) -> Result<()> { let chat = Chat::load_from_db(context, id).await?; + ensure!( + !chat.is_encrypted(context).await?, + "Cannot add email-contacts to encrypted chat {id}" + ); ensure!( chat.typ == Chattype::Broadcast, "{id} is not a broadcast list", @@ -4846,6 +4871,54 @@ async fn set_contacts_by_addrs(context: &Context, id: ChatId, addrs: &[String]) Ok(()) } +/// Set chat contacts by their fingerprints creating the corresponding contacts if necessary. +/// +/// `fingerprint_addrs` is a list of pairs of fingerprint and address. +async fn set_contacts_by_fingerprints( + context: &Context, + id: ChatId, + fingerprint_addrs: &[(String, String)], +) -> Result<()> { + let chat = Chat::load_from_db(context, id).await?; + ensure!( + chat.is_encrypted(context).await?, + "Cannot add PGP-contacts to unencrypted chat {id}" + ); + ensure!( + chat.typ == Chattype::Broadcast, + "{id} is not a broadcast list", + ); + let mut contacts = HashSet::new(); + for (fingerprint, addr) in fingerprint_addrs { + let contact_addr = ContactAddress::new(addr)?; + let contact = Contact::add_or_lookup_ex(context, "", &contact_addr, &fingerprint, Origin::Hidden) + .await? + .0; + contacts.insert(contact); + } + let contacts_old = HashSet::::from_iter(get_chat_contacts(context, id).await?); + if contacts == contacts_old { + return Ok(()); + } + context + .sql + .transaction(move |transaction| { + transaction.execute("DELETE FROM chats_contacts WHERE chat_id=?", (id,))?; + + // We do not care about `add_timestamp` column + // because timestamps are not used for broadcast lists. + let mut statement = transaction + .prepare("INSERT INTO chats_contacts (chat_id, contact_id) VALUES (?, ?)")?; + for contact_id in &contacts { + statement.execute((id, contact_id))?; + } + Ok(()) + }) + .await?; + context.emit_event(EventType::ChatModified(id)); + Ok(()) +} + /// A cross-device chat id used for synchronisation. #[derive(Debug, Serialize, Deserialize, PartialEq)] pub(crate) enum SyncId { @@ -4876,6 +4949,10 @@ pub(crate) enum SyncAction { Rename(String), /// Set chat contacts by their addresses. SetContacts(Vec), + /// Set chat contacts by their fingerprints. + /// + /// The list is a list of pairs of fingerprint and address. + SetPgpContacts(Vec<(String, String)>), Delete, } @@ -4959,6 +5036,7 @@ impl Context { } SyncAction::Rename(to) => rename_ex(self, Nosync, chat_id, to).await, SyncAction::SetContacts(addrs) => set_contacts_by_addrs(self, chat_id, addrs).await, + SyncAction::SetPgpContacts(fingerprint_addrs) => set_contacts_by_fingerprints(self, chat_id, fingerprint_addrs).await, SyncAction::Delete => chat_id.delete_ex(self, Nosync).await, } } diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 4898ce2a7f..314a62cefc 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -3254,7 +3254,7 @@ async fn test_sync_broadcast() -> Result<()> { for a in [alice0, alice1] { a.set_config_bool(Config::SyncMsgs, true).await?; } - let bob = TestContext::new_bob().await; + let bob = &tcm.bob().await; let a0b_contact_id = alice0.add_or_lookup_contact(&bob).await.id; let a0_broadcast_id = create_broadcast_list(alice0).await?; @@ -3270,13 +3270,12 @@ async fn test_sync_broadcast() -> Result<()> { assert!(get_chat_contacts(alice1, a1_broadcast_id).await?.is_empty()); add_contact_to_chat(alice0, a0_broadcast_id, a0b_contact_id).await?; sync(alice0, alice1).await; - let a1b_contact_id = Contact::lookup_id_by_addr( - alice1, - &bob.get_config(Config::Addr).await?.unwrap(), - Origin::Hidden, - ) - .await? - .unwrap(); + + // This also imports Bob's key from the vCard. + // Otherwise it is possible that second device + // does not have Bob's key as only the fingerprint + // is transferred in the sync message. + let a1b_contact_id = alice1.add_or_lookup_contact(bob).await.id; assert_eq!( get_chat_contacts(alice1, a1_broadcast_id).await?, vec![a1b_contact_id] diff --git a/src/contact.rs b/src/contact.rs index a5165437c2..6d5c7948ed 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -1867,7 +1867,10 @@ pub(crate) async fn mark_contact_id_as_verified( contact_id: ContactId, verifier_id: ContactId, ) -> Result<()> { - debug_assert_ne!(contact_id, verifier_id, "Contact cannot be verified by self"); + debug_assert_ne!( + contact_id, verifier_id, + "Contact cannot be verified by self" + ); context .sql .transaction(|transaction| { @@ -1886,7 +1889,9 @@ pub(crate) async fn mark_contact_id_as_verified( |row| row.get(0), )?; if verifier_fingerprint.is_empty() { - bail!("Contact {contact_id} cannot be verified by non-PGP contact {verifier_id}."); + bail!( + "Contact {contact_id} cannot be verified by non-PGP contact {verifier_id}." + ); } } transaction.execute( diff --git a/src/message/message_tests.rs b/src/message/message_tests.rs index 453c88defc..9c82a2b652 100644 --- a/src/message/message_tests.rs +++ b/src/message/message_tests.rs @@ -1,10 +1,7 @@ use num_traits::FromPrimitive; use super::*; -use crate::chat::{ - self, add_contact_to_chat, forward_msgs, marknoticed_chat, save_msgs, send_text_msg, ChatItem, - ProtectionStatus, -}; +use crate::chat::{self, forward_msgs, marknoticed_chat, save_msgs, send_text_msg, ChatItem}; use crate::chatlist::Chatlist; use crate::config::Config; use crate::reaction::send_reaction; @@ -194,7 +191,9 @@ async fn test_unencrypted_quote_encrypted_message() -> Result<()> { tcm.section("Bob sends encrypted message to Alice"); let alice_chat = alice.create_chat(bob).await; - let sent = alice.send_text(alice_chat.id, "Hi! This is encrypted.").await; + let sent = alice + .send_text(alice_chat.id, "Hi! This is encrypted.") + .await; let bob_received_message = bob.recv_msg(&sent).await; assert_eq!(bob_received_message.get_showpadlock(), true); diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 696b8eff97..9b1f7945af 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -4735,10 +4735,7 @@ async fn test_protected_group_add_remove_member_missing_key() -> Result<()> { let alice_bob_id = alice.add_or_lookup_contact(bob).await.id; add_contact_to_chat(alice, group_id, alice_bob_id).await?; alice.send_text(group_id, "Hello!").await; - alice - .sql - .execute("DELETE FROM public_keys", ()) - .await?; + alice.sql.execute("DELETE FROM public_keys", ()).await?; let fiona = &tcm.fiona().await; let fiona_addr = fiona.get_config(Config::Addr).await?.unwrap(); diff --git a/src/securejoin/securejoin_tests.rs b/src/securejoin/securejoin_tests.rs index 140afb0384..74de843932 100644 --- a/src/securejoin/securejoin_tests.rs +++ b/src/securejoin/securejoin_tests.rs @@ -1,17 +1,15 @@ -use deltachat_contact_tools::{ContactAddress, EmailAddress}; +use deltachat_contact_tools::EmailAddress; use super::*; use crate::chat::{remove_contact_from_chat, CantSendReason}; use crate::chatlist::Chatlist; use crate::constants::{self, Chattype}; -use crate::imex::{imex, ImexMode}; use crate::receive_imf::receive_imf; use crate::stock_str::{self, chat_protection_enabled}; use crate::test_utils::{ get_chat_msg, TestContext, TestContextManager, TimeShiftFalsePositiveNote, }; use crate::tools::SystemTime; -use std::collections::HashSet; use std::time::Duration; #[derive(PartialEq)] @@ -234,9 +232,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) { tcm.section("Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm"); alice.recv_msg_trash(&sent).await; assert_eq!(contact_bob.is_verified(&alice).await.unwrap(), true); - let contact_bob = Contact::get_by_id(&alice, contact_bob_id) - .await - .unwrap(); + let contact_bob = Contact::get_by_id(&alice, contact_bob_id).await.unwrap(); assert_eq!(contact_bob.get_authname(), "Bob Examplenet"); assert!(contact_bob.get_name().is_empty()); assert_eq!(contact_bob.is_bot(), false); @@ -465,9 +461,7 @@ async fn test_secure_join() -> Result<()> { chat::create_group_chat(&alice, ProtectionStatus::Protected, "the chat").await?; tcm.section("Step 1: Generate QR-code, secure-join implied by chatid"); - let qr = get_securejoin_qr(&alice, Some(alice_chatid)) - .await - .unwrap(); + let qr = get_securejoin_qr(&alice, Some(alice_chatid)).await.unwrap(); tcm.section("Step 2: Bob scans QR-code, sends vg-request"); let bob_chatid = join_securejoin(&bob, &qr).await?; diff --git a/src/tests/aeap.rs b/src/tests/aeap.rs index a26b89f305..653ae5ba81 100644 --- a/src/tests/aeap.rs +++ b/src/tests/aeap.rs @@ -1,9 +1,7 @@ use anyhow::Result; use crate::chat::{self, Chat, ChatId, ProtectionStatus}; -use crate::contact; -use crate::contact::Contact; -use crate::contact::ContactId; +use crate::contact::{Contact, ContactId}; use crate::message::Message; use crate::receive_imf::receive_imf; use crate::securejoin::get_securejoin_qr; diff --git a/src/tests/verified_chats.rs b/src/tests/verified_chats.rs index 72a8fd8d84..76dfbdade6 100644 --- a/src/tests/verified_chats.rs +++ b/src/tests/verified_chats.rs @@ -4,9 +4,8 @@ use pretty_assertions::assert_eq; use crate::chat::{ self, add_contact_to_chat, remove_contact_from_chat, send_msg, Chat, ProtectionStatus, }; -use crate::chatlist::Chatlist; use crate::config::Config; -use crate::constants::{Chattype, DC_GCL_FOR_FORWARDING}; +use crate::constants::Chattype; use crate::contact::{Contact, ContactId}; use crate::message::Message; use crate::mimefactory::MimeFactory; From 465e7ad5610bad5402f24ecac7efbceb4ca5e8c4 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 24 Apr 2025 01:31:26 +0000 Subject: [PATCH 126/381] rename certificates into keys --- src/chat.rs | 14 +++++++---- src/contact.rs | 16 ++++++------- src/contact/contact_tests.rs | 2 +- src/mimefactory.rs | 45 +++++++++++++++++------------------- src/qr.rs | 2 +- 5 files changed, 40 insertions(+), 39 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index e1c4e2c71a..c1e7da9314 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -2304,7 +2304,8 @@ impl Chat { |addrs| addrs.collect::, _>>().map_err(Into::into), ) .await?; - self.sync(context, SyncAction::SetPgpContacts(fingerprint_addrs)).await?; + self.sync(context, SyncAction::SetPgpContacts(fingerprint_addrs)) + .await?; } else { let addrs = context .sql @@ -4891,9 +4892,10 @@ async fn set_contacts_by_fingerprints( let mut contacts = HashSet::new(); for (fingerprint, addr) in fingerprint_addrs { let contact_addr = ContactAddress::new(addr)?; - let contact = Contact::add_or_lookup_ex(context, "", &contact_addr, &fingerprint, Origin::Hidden) - .await? - .0; + let contact = + Contact::add_or_lookup_ex(context, "", &contact_addr, &fingerprint, Origin::Hidden) + .await? + .0; contacts.insert(contact); } let contacts_old = HashSet::::from_iter(get_chat_contacts(context, id).await?); @@ -5036,7 +5038,9 @@ impl Context { } SyncAction::Rename(to) => rename_ex(self, Nosync, chat_id, to).await, SyncAction::SetContacts(addrs) => set_contacts_by_addrs(self, chat_id, addrs).await, - SyncAction::SetPgpContacts(fingerprint_addrs) => set_contacts_by_fingerprints(self, chat_id, fingerprint_addrs).await, + SyncAction::SetPgpContacts(fingerprint_addrs) => { + set_contacts_by_fingerprints(self, chat_id, fingerprint_addrs).await + } SyncAction::Delete => chat_id.delete_ex(self, Nosync).await, } } diff --git a/src/contact.rs b/src/contact.rs index 6d5c7948ed..3446d67f61 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -265,7 +265,7 @@ pub async fn make_vcard(context: &Context, contacts: &[ContactId]) -> Result None, Some(path) => tokio::fs::read(path) @@ -1385,19 +1385,19 @@ impl Contact { } } - /// Returns OpenPGP certificate of a contact. + /// Returns OpenPGP public key of a contact. /// /// Returns `None` if the contact is not a PGP-contact /// or if the key is not available. - /// It is possible for a PGP-contact to not have a certificate, + /// It is possible for a PGP-contact to not have a key, /// e.g. if only the fingerprint is known from a QR-code. - pub async fn openpgp_certificate(&self, context: &Context) -> Result> { + pub async fn public_key(&self, context: &Context) -> Result> { if self.id == ContactId::SELF { return Ok(Some(load_self_public_key(context).await?)); } if let Some(fingerprint) = &self.fingerprint { - if let Some(certificate_bytes) = context + if let Some(public_key_bytes) = context .sql .query_row_optional( "SELECT public_key @@ -1411,8 +1411,8 @@ impl Contact { ) .await? { - let certificate = SignedPublicKey::from_slice(&certificate_bytes)?; - Ok(Some(certificate)) + let public_key = SignedPublicKey::from_slice(&public_key_bytes)?; + Ok(Some(public_key)) } else { Ok(None) } @@ -1519,7 +1519,7 @@ impl Contact { // We don't need to check if we have our own key. return Ok(true); } - Ok(self.openpgp_certificate(context).await?.is_some()) + Ok(self.public_key(context).await?.is_some()) } /// Returns true if the contact diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs index 90d74b871c..666c2381d3 100644 --- a/src/contact/contact_tests.rs +++ b/src/contact/contact_tests.rs @@ -1037,7 +1037,7 @@ async fn test_make_n_import_vcard() -> Result<()> { let bob_id = alice.recv_msg(&sent_msg).await.from_id; let bob_contact = Contact::get_by_id(alice, bob_id).await?; let key_base64 = bob_contact - .openpgp_certificate(alice) + .public_key(alice) .await? .unwrap() .to_base64(); diff --git a/src/mimefactory.rs b/src/mimefactory.rs index a1b96a5ef9..2c6e7d9579 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -85,11 +85,12 @@ pub struct MimeFactory { /// but `MimeFactory` is not responsible for this. recipients: Vec, - /// Vector of recipient OpenPGP certificates + /// Vector of pairs of recipient + /// addresses and OpenPGP keys /// to use for encryption. /// /// `None` if the message is not encrypted. - encryption_certificates: Option>, + encryption_keys: Option>, /// Vector of pairs of recipient name and address that goes into the `To` field. /// @@ -192,12 +193,12 @@ impl MimeFactory { let mut recipient_ids = HashSet::new(); let mut req_mdn = false; - let encryption_certificates; + let encryption_keys; if chat.is_self_talk() { to.push((from_displayname.to_string(), from_addr.to_string())); - encryption_certificates = if msg + encryption_keys = if msg .param .get_bool(Param::ForcePlaintext) .unwrap_or_default() @@ -216,7 +217,7 @@ impl MimeFactory { recipients.push(list_post.to_string()); // Do not encrypt messages to mailing lists. - encryption_certificates = None; + encryption_keys = None; } else { let email_to_remove = if msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup { msg.param.get(Param::Arg) @@ -235,7 +236,7 @@ impl MimeFactory { || chat.is_encrypted(context).await? }; - let mut certificates = Vec::new(); + let mut keys = Vec::new(); let mut missing_key_addresses = BTreeSet::new(); context .sql @@ -305,7 +306,7 @@ impl MimeFactory { } if let Some(certificate) = certificate_opt { - certificates.push((addr.clone(), certificate)) + keys.push((addr.clone(), certificate)) } else if id != ContactId::SELF { missing_key_addresses.insert(addr.clone()); if is_encrypted { @@ -338,7 +339,7 @@ impl MimeFactory { req_mdn = true; } - encryption_certificates = if !is_encrypted { + encryption_keys = if !is_encrypted { None } else { // Remove recipients for which the key is missing. @@ -346,7 +347,7 @@ impl MimeFactory { recipients.retain(|addr| !missing_key_addresses.contains(addr)); } - Some(certificates) + Some(keys) }; } let (in_reply_to, references) = context @@ -388,7 +389,7 @@ impl MimeFactory { sender_displayname, selfstatus, recipients, - encryption_certificates, + encryption_keys, to, past_members, member_timestamps, @@ -415,9 +416,9 @@ impl MimeFactory { let timestamp = create_smeared_timestamp(context); let addr = contact.get_addr().to_string(); - let encryption_certificates = if contact.is_pgp_contact() { - if let Some(openpgp_certificate) = contact.openpgp_certificate(context).await? { - Some(vec![(addr.clone(), openpgp_certificate)]) + let encryption_keys = if contact.is_pgp_contact() { + if let Some(key) = contact.public_key(context).await? { + Some(vec![(addr.clone(), key)]) } else { Some(Vec::new()) } @@ -431,7 +432,7 @@ impl MimeFactory { sender_displayname: None, selfstatus: "".to_string(), recipients: vec![addr], - encryption_certificates, + encryption_keys, to: vec![("".to_string(), contact.get_addr().to_string())], past_members: vec![], member_timestamps: vec![], @@ -764,7 +765,7 @@ impl MimeFactory { )); } - let is_encrypted = self.encryption_certificates.is_some(); + let is_encrypted = self.encryption_keys.is_some(); // Add ephemeral timer for non-MDN messages. // For MDNs it does not matter because they are not visible @@ -925,7 +926,7 @@ impl MimeFactory { } } - let outer_message = if let Some(encryption_certificates) = self.encryption_certificates { + let outer_message = if let Some(encryption_keys) = self.encryption_keys { // Store protected headers in the inner message. let message = protected_headers .into_iter() @@ -941,8 +942,8 @@ impl MimeFactory { }); // Add gossip headers in chats with multiple recipients - let multiple_recipients = encryption_certificates.len() > 1 - || context.get_config_bool(Config::BccSelf).await?; + let multiple_recipients = + encryption_keys.len() > 1 || context.get_config_bool(Config::BccSelf).await?; let gossip_period = context.get_config_i64(Config::GossipPeriod).await?; let now = time(); @@ -950,7 +951,7 @@ impl MimeFactory { match &self.loaded { Loaded::Message { chat, msg } => { if chat.typ != Chattype::Broadcast { - for (addr, key) in &encryption_certificates { + for (addr, key) in &encryption_keys { let fingerprint = key.dc_fingerprint().hex(); let cmd = msg.param.get_cmd(); let should_do_gossip = cmd == SystemMessage::MemberAddedToGroup @@ -1035,11 +1036,7 @@ impl MimeFactory { // Encrypt to self unconditionally, // even for a single-device setup. let mut encryption_keyring = vec![encrypt_helper.public_key.clone()]; - encryption_keyring.extend( - encryption_certificates - .iter() - .map(|(_addr, key)| (*key).clone()), - ); + encryption_keyring.extend(encryption_keys.iter().map(|(_addr, key)| (*key).clone())); // XXX: additional newline is needed // to pass filtermail at diff --git a/src/qr.rs b/src/qr.rs index 50f99acc28..87167f58ff 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -532,7 +532,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result { .await?; let contact = Contact::get_by_id(context, contact_id).await?; - if contact.openpgp_certificate(context).await?.is_some() { + if contact.public_key(context).await?.is_some() { Ok(Qr::FprOk { contact_id }) } else { Ok(Qr::FprMismatch { From a111fdce8589646cc3fafd1f624c671e78709d31 Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 21 Apr 2025 10:14:34 +0000 Subject: [PATCH 127/381] Chat-Group-Past-Member-Fingerprints --- src/headerdef.rs | 3 +++ src/mimefactory.rs | 41 ++++++++++++++++++++++++++++++++++------- src/mimeparser.rs | 10 ++++++++++ src/receive_imf.rs | 7 ++++++- src/securejoin.rs | 1 - 5 files changed, 53 insertions(+), 9 deletions(-) diff --git a/src/headerdef.rs b/src/headerdef.rs index cc49fa9a28..6819e5321f 100644 --- a/src/headerdef.rs +++ b/src/headerdef.rs @@ -69,6 +69,9 @@ pub enum HeaderDef { /// Past members of the group. ChatGroupPastMembers, + /// Fingerprints of past members. + ChatGroupPastMembersFpr, + /// Space-separated timestamps of member addition /// for members listed in the `To` field /// followed by timestamps of member removal diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 2c6e7d9579..0dcf9dd5d4 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -103,6 +103,9 @@ pub struct MimeFactory { /// Vector of pairs of past group member names and addresses. past_members: Vec<(String, String)>, + /// Vector of OpenPGP fingerprints for past members. + past_member_fingerprints: Vec, + /// Timestamps of the members in the same order as in the `to` /// followed by `past_members`. /// @@ -189,6 +192,7 @@ impl MimeFactory { let mut recipients = Vec::new(); let mut to = Vec::new(); let mut past_members = Vec::new(); + let mut past_member_fingerprints = Vec::new(); let mut member_timestamps = Vec::new(); let mut recipient_ids = HashSet::new(); let mut req_mdn = false; @@ -241,7 +245,7 @@ impl MimeFactory { context .sql .query_map( - "SELECT c.authname, c.addr, c.id, cc.add_timestamp, cc.remove_timestamp, k.public_key + "SELECT c.authname, c.addr, c.fingerprint, c.id, cc.add_timestamp, cc.remove_timestamp, k.public_key FROM chats_contacts cc LEFT JOIN contacts c ON cc.contact_id=c.id LEFT JOIN public_keys k ON k.fingerprint=c.fingerprint @@ -250,17 +254,18 @@ impl MimeFactory { |row| { let authname: String = row.get(0)?; let addr: String = row.get(1)?; - let id: ContactId = row.get(2)?; - let add_timestamp: i64 = row.get(3)?; - let remove_timestamp: i64 = row.get(4)?; - let certificate_bytes_opt: Option> = row.get(5)?; - Ok((authname, addr, id, add_timestamp, remove_timestamp, certificate_bytes_opt)) + let fingerprint: String = row.get(2)?; + let id: ContactId = row.get(3)?; + let add_timestamp: i64 = row.get(4)?; + let remove_timestamp: i64 = row.get(5)?; + let certificate_bytes_opt: Option> = row.get(6)?; + Ok((authname, addr, fingerprint, id, add_timestamp, remove_timestamp, certificate_bytes_opt)) }, |rows| { let mut past_member_timestamps = Vec::new(); for row in rows { - let (authname, addr, id, add_timestamp, remove_timestamp, certificate_bytes) = row?; + let (authname, addr, fingerprint, id, add_timestamp, remove_timestamp, certificate_bytes) = row?; let certificate_opt = if let Some(certificate_bytes) = certificate_bytes { Some(SignedPublicKey::from_slice(&certificate_bytes)?) @@ -301,6 +306,12 @@ impl MimeFactory { if !undisclosed_recipients { past_members.push((name, addr.clone())); past_member_timestamps.push(remove_timestamp); + + if !fingerprint.is_empty() { + past_member_fingerprints.push(fingerprint); + } else { + debug_assert!(past_member_fingerprints.is_empty(), "If some past member is a PGP-contact, all other past members should be PGP-contacts too"); + } } } } @@ -392,6 +403,7 @@ impl MimeFactory { encryption_keys, to, past_members, + past_member_fingerprints, member_timestamps, timestamp: msg.timestamp_sort, loaded: Loaded::Message { msg, chat }, @@ -435,6 +447,7 @@ impl MimeFactory { encryption_keys, to: vec![("".to_string(), contact.get_addr().to_string())], past_members: vec![], + past_member_fingerprints: vec![], member_timestamps: vec![], timestamp, loaded: Loaded::Mdn { @@ -624,6 +637,20 @@ impl MimeFactory { "Chat-Group-Past-Members", mail_builder::headers::address::Address::new_list(past_members.clone()).into(), )); + + if !self.past_member_fingerprints.is_empty() { + headers.push(( + "Chat-Group-Past-Members-Fpr", + mail_builder::headers::raw::Raw::new( + self.past_member_fingerprints + .iter() + .map(|fp| fp.to_string()) + .collect::>() + .join(" "), + ) + .into() + )); + } } if let Loaded::Message { chat, .. } = &self.loaded { diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 8fe5cd6ce2..d4820f0286 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -1858,6 +1858,16 @@ impl MimeMessage { .collect() }) } + + /// Returns list of fingerprints from + /// `Chat-Group-Past-Members-Fpr` header. + pub fn chat_group_past_members_fingerprints(&self) -> Vec { + if let Some(header) = self.get_header(HeaderDef::ChatGroupPastMembersFpr) { + header.split_ascii_whitespace().filter_map(|fpr| fpr.parse::().ok()).collect() + } else { + Vec::new() + } + } } fn remove_header( diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 9407eabf2a..b68296b809 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -334,6 +334,7 @@ pub(crate) async fn receive_imf_inner( context, &mime_parser.recipients, &mime_parser.gossiped_keys, + &[], // TODO use To fingerprints if !mime_parser.incoming { Origin::OutgoingTo } else if incoming_origin.is_known() { @@ -346,9 +347,11 @@ pub(crate) async fn receive_imf_inner( if mime_parser.get_chat_group_id().is_some() { to_ids = pgp_to_ids; + let past_members_fingerprints = mime_parser.chat_group_past_members_fingerprints(); + if let Some(chat_id) = chat_id { past_ids = - lookup_pgp_contacts_by_address_list(context, &mime_parser.past_members, chat_id) + lookup_pgp_contacts_by_address_list(context, &mime_parser.past_members, &past_members_fingerprints, chat_id) .await?; } else { // TODO: lookup by fingerprints if they are available. @@ -3169,6 +3172,7 @@ async fn add_or_lookup_pgp_contacts_by_address_list( context: &Context, address_list: &[SingleInfo], gossiped_keys: &HashMap, + fingerprints: &[Fingerprint], origin: Origin, ) -> Result>> { let mut contact_ids = Vec::new(); @@ -3272,6 +3276,7 @@ async fn lookup_pgp_contact_by_fingerprint( async fn lookup_pgp_contacts_by_address_list( context: &Context, address_list: &[SingleInfo], + fingerprints: &[Fingerprint], chat_id: ChatId, ) -> Result>> { let mut contact_ids = Vec::new(); diff --git a/src/securejoin.rs b/src/securejoin.rs index e747a00d66..574908634b 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -23,7 +23,6 @@ use crate::securejoin::bob::JoinerProgress; use crate::stock_str; use crate::sync::Sync::*; use crate::token; -use crate::tools::time; mod bob; mod qrinvite; From bf163415d3d39930bd0d30492b05d3b3ca2371e3 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 24 Apr 2025 21:30:20 +0000 Subject: [PATCH 128/381] Do not encrypt to past members and do not gossip their keys --- src/mimefactory.rs | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 0dcf9dd5d4..8a5935c8f4 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -245,11 +245,20 @@ impl MimeFactory { context .sql .query_map( - "SELECT c.authname, c.addr, c.fingerprint, c.id, cc.add_timestamp, cc.remove_timestamp, k.public_key + "SELECT + c.authname, + c.addr, + c.fingerprint, + c.id, + cc.add_timestamp, + cc.remove_timestamp, + k.public_key FROM chats_contacts cc LEFT JOIN contacts c ON cc.contact_id=c.id LEFT JOIN public_keys k ON k.fingerprint=c.fingerprint - WHERE cc.chat_id=? AND (cc.contact_id>9 OR (cc.contact_id=1 AND ?))", + WHERE cc.chat_id=? + AND cc.add_timestamp >= cc.remove_timestamp + AND (cc.contact_id>9 OR (cc.contact_id=1 AND ?))", (msg.chat_id, chat.typ == Chattype::Group), |row| { let authname: String = row.get(0)?; @@ -258,17 +267,17 @@ impl MimeFactory { let id: ContactId = row.get(3)?; let add_timestamp: i64 = row.get(4)?; let remove_timestamp: i64 = row.get(5)?; - let certificate_bytes_opt: Option> = row.get(6)?; - Ok((authname, addr, fingerprint, id, add_timestamp, remove_timestamp, certificate_bytes_opt)) + let public_key_bytes_opt: Option> = row.get(6)?; + Ok((authname, addr, fingerprint, id, add_timestamp, remove_timestamp, public_key_bytes_opt)) }, |rows| { let mut past_member_timestamps = Vec::new(); for row in rows { - let (authname, addr, fingerprint, id, add_timestamp, remove_timestamp, certificate_bytes) = row?; + let (authname, addr, fingerprint, id, add_timestamp, remove_timestamp, public_key_bytes_opt) = row?; - let certificate_opt = if let Some(certificate_bytes) = certificate_bytes { - Some(SignedPublicKey::from_slice(&certificate_bytes)?) + let public_key_opt = if let Some(ref public_key_bytes) = public_key_bytes_opt { + Some(SignedPublicKey::from_slice(public_key_bytes)?) } else { None }; @@ -316,8 +325,8 @@ impl MimeFactory { } } - if let Some(certificate) = certificate_opt { - keys.push((addr.clone(), certificate)) + if let Some(public_key) = public_key_opt { + keys.push((addr.clone(), public_key)) } else if id != ContactId::SELF { missing_key_addresses.insert(addr.clone()); if is_encrypted { From 9a575e876e6755e20886046fac30ad7a47fc9ded Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 25 Apr 2025 02:54:28 +0000 Subject: [PATCH 129/381] chat::chat_tests::test_past_members fixed --- src/mimefactory.rs | 19 +++++++++---------- src/receive_imf.rs | 29 ++++++++++++++++++----------- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 8a5935c8f4..dc6995c3cb 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -257,7 +257,6 @@ impl MimeFactory { LEFT JOIN contacts c ON cc.contact_id=c.id LEFT JOIN public_keys k ON k.fingerprint=c.fingerprint WHERE cc.chat_id=? - AND cc.add_timestamp >= cc.remove_timestamp AND (cc.contact_id>9 OR (cc.contact_id=1 AND ?))", (msg.chat_id, chat.typ == Chattype::Group), |row| { @@ -300,6 +299,15 @@ impl MimeFactory { } } recipient_ids.insert(id); + + if let Some(public_key) = public_key_opt { + keys.push((addr.clone(), public_key)) + } else if id != ContactId::SELF { + missing_key_addresses.insert(addr.clone()); + if is_encrypted { + warn!(context, "Missing key for {addr}"); + } + } } else if remove_timestamp.saturating_add(60 * 24 * 3600) > now { // Row is a tombstone, // member is not actually part of the group. @@ -324,15 +332,6 @@ impl MimeFactory { } } } - - if let Some(public_key) = public_key_opt { - keys.push((addr.clone(), public_key)) - } else if id != ContactId::SELF { - missing_key_addresses.insert(addr.clone()); - if is_encrypted { - warn!(context, "Missing key for {addr}"); - } - } } debug_assert!(member_timestamps.len() >= to.len()); diff --git a/src/receive_imf.rs b/src/receive_imf.rs index b68296b809..f435abef14 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -335,13 +335,7 @@ pub(crate) async fn receive_imf_inner( &mime_parser.recipients, &mime_parser.gossiped_keys, &[], // TODO use To fingerprints - if !mime_parser.incoming { - Origin::OutgoingTo - } else if incoming_origin.is_known() { - Origin::IncomingTo - } else { - Origin::IncomingUnknownTo - }, + Origin::Hidden ) .await?; @@ -354,8 +348,14 @@ pub(crate) async fn receive_imf_inner( lookup_pgp_contacts_by_address_list(context, &mime_parser.past_members, &past_members_fingerprints, chat_id) .await?; } else { - // TODO: lookup by fingerprints if they are available. - past_ids = vec![None; mime_parser.past_members.len()]; + past_ids = add_or_lookup_pgp_contacts_by_address_list( + context, + &mime_parser.past_members, + &mime_parser.gossiped_keys, + &past_members_fingerprints, + Origin::Hidden + ) + .await?; } } else { if pgp_to_ids.len() == 1 @@ -3176,17 +3176,22 @@ async fn add_or_lookup_pgp_contacts_by_address_list( origin: Origin, ) -> Result>> { let mut contact_ids = Vec::new(); + let mut fingerprint_iter = fingerprints.iter(); for info in address_list { let addr = &info.addr; if !may_be_valid_addr(addr) { contact_ids.push(None); continue; } - let Some(key) = gossiped_keys.get(addr) else { + let fingerprint: String = if let Some(fp) = fingerprint_iter.next() { + // Iterator has not ran out of fingerprints yet. + fp.hex() + } else if let Some(key) = gossiped_keys.get(addr) { + key.dc_fingerprint().hex() + } else { contact_ids.push(None); continue; }; - let fingerprint = key.dc_fingerprint().hex(); let display_name = info.display_name.as_deref(); if let Ok(addr) = ContactAddress::new(addr) { let (contact_id, _) = Contact::add_or_lookup_ex( @@ -3279,6 +3284,8 @@ async fn lookup_pgp_contacts_by_address_list( fingerprints: &[Fingerprint], chat_id: ChatId, ) -> Result>> { + // TODO: create a contact with a given fingerprint + // if fingerprint is available. let mut contact_ids = Vec::new(); for info in address_list { let addr = &info.addr; From b6df044f99b777c34485ce03d72305ddcee29ae0 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 25 Apr 2025 18:48:40 +0000 Subject: [PATCH 130/381] single fingerprints header --- src/contact/contact_tests.rs | 6 +-- src/headerdef.rs | 6 +-- src/mimefactory.rs | 85 ++++++++++++++++++++++-------------- src/mimeparser.rs | 11 +++-- src/receive_imf.rs | 31 +++++++++---- 5 files changed, 86 insertions(+), 53 deletions(-) diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs index 666c2381d3..98d41254ee 100644 --- a/src/contact/contact_tests.rs +++ b/src/contact/contact_tests.rs @@ -1036,11 +1036,7 @@ async fn test_make_n_import_vcard() -> Result<()> { let sent_msg = bob.send_text(chat.id, "moin").await; let bob_id = alice.recv_msg(&sent_msg).await.from_id; let bob_contact = Contact::get_by_id(alice, bob_id).await?; - let key_base64 = bob_contact - .public_key(alice) - .await? - .unwrap() - .to_base64(); + let key_base64 = bob_contact.public_key(alice).await?.unwrap().to_base64(); let fiona_id = Contact::create(alice, "Fiona", "fiona@example.net").await?; assert_eq!(make_vcard(alice, &[]).await?, "".to_string()); diff --git a/src/headerdef.rs b/src/headerdef.rs index 6819e5321f..95236c245d 100644 --- a/src/headerdef.rs +++ b/src/headerdef.rs @@ -69,15 +69,15 @@ pub enum HeaderDef { /// Past members of the group. ChatGroupPastMembers, - /// Fingerprints of past members. - ChatGroupPastMembersFpr, - /// Space-separated timestamps of member addition /// for members listed in the `To` field /// followed by timestamps of member removal /// for members listed in the `Chat-Group-Past-Members` field. ChatGroupMemberTimestamps, + /// Fingerprints of the To header. + ChatGroupMemberFpr, + /// Duration of the attached media file. ChatDuration, diff --git a/src/mimefactory.rs b/src/mimefactory.rs index dc6995c3cb..6255d9ab3b 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -15,6 +15,7 @@ use tokio::fs; use crate::aheader::{Aheader, EncryptPreference}; use crate::blob::BlobObject; +use crate::key::load_self_public_key; use crate::chat::{self, Chat}; use crate::config::Config; use crate::constants::ASM_SUBJECT; @@ -103,14 +104,18 @@ pub struct MimeFactory { /// Vector of pairs of past group member names and addresses. past_members: Vec<(String, String)>, - /// Vector of OpenPGP fingerprints for past members. - past_member_fingerprints: Vec, + /// Fingerprints of the members in the same order as in the `to` + /// followed by `past_members`. + /// + /// If this is not empty, its length + /// should be the sum of `to` and `past_members` length. + member_fingerprints: Vec, /// Timestamps of the members in the same order as in the `to` /// followed by `past_members`. /// /// If this is not empty, its length - /// should be the sum of `recipients` and `past_members` length. + /// should be the sum of `to` and `past_members` length. member_timestamps: Vec, timestamp: i64, @@ -192,13 +197,16 @@ impl MimeFactory { let mut recipients = Vec::new(); let mut to = Vec::new(); let mut past_members = Vec::new(); - let mut past_member_fingerprints = Vec::new(); + let mut member_fingerprints = Vec::new(); let mut member_timestamps = Vec::new(); let mut recipient_ids = HashSet::new(); let mut req_mdn = false; let encryption_keys; + let self_public_key = load_self_public_key(context).await?; + let self_fingerprint = self_public_key.dc_fingerprint().hex(); + if chat.is_self_talk() { to.push((from_displayname.to_string(), from_addr.to_string())); @@ -271,6 +279,7 @@ impl MimeFactory { }, |rows| { let mut past_member_timestamps = Vec::new(); + let mut past_member_fingerprints = Vec::new(); for row in rows { let (authname, addr, fingerprint, id, add_timestamp, remove_timestamp, public_key_bytes_opt) = row?; @@ -295,6 +304,14 @@ impl MimeFactory { recipients.push(addr.clone()); if !undisclosed_recipients { to.push((name, addr.clone())); + + if !fingerprint.is_empty() { + member_fingerprints.push(fingerprint); + } else if id == ContactId::SELF { + member_fingerprints.push(self_fingerprint.clone()); + } else { + debug_assert!(member_fingerprints.is_empty(), "If some past member is a PGP-contact, all other past members should be PGP-contacts too"); + } member_timestamps.push(add_timestamp); } } @@ -335,6 +352,7 @@ impl MimeFactory { } debug_assert!(member_timestamps.len() >= to.len()); + debug_assert!(member_fingerprints.is_empty() || member_fingerprints.len() >= to.len()); if to.len() > 1 { if let Some(position) = to.iter().position(|(_, x)| x == &from_addr) { @@ -344,6 +362,7 @@ impl MimeFactory { } member_timestamps.extend(past_member_timestamps); + member_fingerprints.extend(past_member_fingerprints); Ok(()) }, ) @@ -411,7 +430,7 @@ impl MimeFactory { encryption_keys, to, past_members, - past_member_fingerprints, + member_fingerprints, member_timestamps, timestamp: msg.timestamp_sort, loaded: Loaded::Message { msg, chat }, @@ -455,7 +474,7 @@ impl MimeFactory { encryption_keys, to: vec![("".to_string(), contact.get_addr().to_string())], past_members: vec![], - past_member_fingerprints: vec![], + member_fingerprints: vec![], member_timestamps: vec![], timestamp, loaded: Loaded::Mdn { @@ -645,38 +664,38 @@ impl MimeFactory { "Chat-Group-Past-Members", mail_builder::headers::address::Address::new_list(past_members.clone()).into(), )); - - if !self.past_member_fingerprints.is_empty() { - headers.push(( - "Chat-Group-Past-Members-Fpr", - mail_builder::headers::raw::Raw::new( - self.past_member_fingerprints - .iter() - .map(|fp| fp.to_string()) - .collect::>() - .join(" "), - ) - .into() - )); - } } if let Loaded::Message { chat, .. } = &self.loaded { if chat.typ == Chattype::Group - && !self.member_timestamps.is_empty() - && !chat.member_list_is_stale(context).await? { - headers.push(( - "Chat-Group-Member-Timestamps", - mail_builder::headers::raw::Raw::new( - self.member_timestamps - .iter() - .map(|ts| ts.to_string()) - .collect::>() - .join(" "), - ) - .into(), - )); + if !self.member_timestamps.is_empty() && !chat.member_list_is_stale(context).await? { + headers.push(( + "Chat-Group-Member-Timestamps", + mail_builder::headers::raw::Raw::new( + self.member_timestamps + .iter() + .map(|ts| ts.to_string()) + .collect::>() + .join(" "), + ) + .into(), + )); + } + + if !self.member_fingerprints.is_empty() { + headers.push(( + "Chat-Group-Member-Fpr", + mail_builder::headers::raw::Raw::new( + self.member_fingerprints + .iter() + .map(|fp| fp.to_string()) + .collect::>() + .join(" "), + ) + .into(), + )); + } } } diff --git a/src/mimeparser.rs b/src/mimeparser.rs index d4820f0286..3eb03208a4 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -1860,10 +1860,13 @@ impl MimeMessage { } /// Returns list of fingerprints from - /// `Chat-Group-Past-Members-Fpr` header. - pub fn chat_group_past_members_fingerprints(&self) -> Vec { - if let Some(header) = self.get_header(HeaderDef::ChatGroupPastMembersFpr) { - header.split_ascii_whitespace().filter_map(|fpr| fpr.parse::().ok()).collect() + /// `Chat-Group-Member-Fpr` header. + pub fn chat_group_member_fingerprints(&self) -> Vec { + if let Some(header) = self.get_header(HeaderDef::ChatGroupMemberFpr) { + header + .split_ascii_whitespace() + .filter_map(|fpr| fpr.parse::().ok()) + .collect() } else { Vec::new() } diff --git a/src/receive_imf.rs b/src/receive_imf.rs index f435abef14..c9cde1bc70 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -330,30 +330,45 @@ pub(crate) async fn receive_imf_inner( let to_ids: Vec>; let past_ids: Vec>; + let member_fingerprints = mime_parser.chat_group_member_fingerprints(); + let to_member_fingerprints; + let past_member_fingerprints; + + if member_fingerprints.len() > mime_parser.recipients.len() { + (to_member_fingerprints, past_member_fingerprints) = member_fingerprints.split_at(mime_parser.recipients.len()); + } else { + warn!(context, "Unexpected length of the fingerprint header."); + to_member_fingerprints = &[]; + past_member_fingerprints = &[]; + }; + let pgp_to_ids = add_or_lookup_pgp_contacts_by_address_list( context, &mime_parser.recipients, &mime_parser.gossiped_keys, - &[], // TODO use To fingerprints - Origin::Hidden + &to_member_fingerprints, + Origin::Hidden, ) .await?; if mime_parser.get_chat_group_id().is_some() { to_ids = pgp_to_ids; - let past_members_fingerprints = mime_parser.chat_group_past_members_fingerprints(); if let Some(chat_id) = chat_id { - past_ids = - lookup_pgp_contacts_by_address_list(context, &mime_parser.past_members, &past_members_fingerprints, chat_id) - .await?; + past_ids = lookup_pgp_contacts_by_address_list( + context, + &mime_parser.past_members, + &past_member_fingerprints, + chat_id, + ) + .await?; } else { past_ids = add_or_lookup_pgp_contacts_by_address_list( context, &mime_parser.past_members, &mime_parser.gossiped_keys, - &past_members_fingerprints, - Origin::Hidden + &past_member_fingerprints, + Origin::Hidden, ) .await?; } From cede2d273757d02ba685281f66fedd63949b39df Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 25 Apr 2025 18:59:37 +0000 Subject: [PATCH 131/381] hidden origin for past members --- src/receive_imf.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index c9cde1bc70..0d9b00860f 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -400,13 +400,7 @@ pub(crate) async fn receive_imf_inner( past_ids = add_or_lookup_contacts_by_address_list( context, &mime_parser.past_members, - if !mime_parser.incoming { - Origin::OutgoingTo - } else if incoming_origin.is_known() { - Origin::IncomingTo - } else { - Origin::IncomingUnknownTo - }, + Origin::Hidden ) .await?; }; From f4020c8d91c1542714949c82dab7a8d43c47a2d8 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 25 Apr 2025 19:33:18 +0000 Subject: [PATCH 132/381] fixup --- src/mimefactory.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 6255d9ab3b..f476a4422b 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -358,6 +358,7 @@ impl MimeFactory { if let Some(position) = to.iter().position(|(_, x)| x == &from_addr) { to.remove(position); member_timestamps.remove(position); + member_fingerprints.remove(position); } } From 2147c1f6db3db032f7cbb86ee0d296c143207eb4 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 25 Apr 2025 19:39:05 +0000 Subject: [PATCH 133/381] fixup --- src/mimefactory.rs | 8 ++++---- src/receive_imf.rs | 14 ++++++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index f476a4422b..93c82acbac 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -15,7 +15,6 @@ use tokio::fs; use crate::aheader::{Aheader, EncryptPreference}; use crate::blob::BlobObject; -use crate::key::load_self_public_key; use crate::chat::{self, Chat}; use crate::config::Config; use crate::constants::ASM_SUBJECT; @@ -24,6 +23,7 @@ use crate::contact::{Contact, ContactId, Origin}; use crate::context::Context; use crate::e2ee::EncryptHelper; use crate::ephemeral::Timer as EphemeralTimer; +use crate::key::load_self_public_key; use crate::key::{DcKey, SignedPublicKey}; use crate::location; use crate::message::{self, Message, MsgId, Viewtype}; @@ -668,9 +668,9 @@ impl MimeFactory { } if let Loaded::Message { chat, .. } = &self.loaded { - if chat.typ == Chattype::Group - { - if !self.member_timestamps.is_empty() && !chat.member_list_is_stale(context).await? { + if chat.typ == Chattype::Group { + if !self.member_timestamps.is_empty() && !chat.member_list_is_stale(context).await? + { headers.push(( "Chat-Group-Member-Timestamps", mail_builder::headers::raw::Raw::new( diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 0d9b00860f..e50b51011e 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -334,10 +334,16 @@ pub(crate) async fn receive_imf_inner( let to_member_fingerprints; let past_member_fingerprints; - if member_fingerprints.len() > mime_parser.recipients.len() { - (to_member_fingerprints, past_member_fingerprints) = member_fingerprints.split_at(mime_parser.recipients.len()); + if member_fingerprints.len() >= mime_parser.recipients.len() { + (to_member_fingerprints, past_member_fingerprints) = + member_fingerprints.split_at(mime_parser.recipients.len()); } else { - warn!(context, "Unexpected length of the fingerprint header."); + warn!( + context, + "Unexpected length of the fingerprint header, expected at least {}, got {}.", + mime_parser.recipients.len(), + member_fingerprints.len() + ); to_member_fingerprints = &[]; past_member_fingerprints = &[]; }; @@ -400,7 +406,7 @@ pub(crate) async fn receive_imf_inner( past_ids = add_or_lookup_contacts_by_address_list( context, &mime_parser.past_members, - Origin::Hidden + Origin::Hidden, ) .await?; }; From 48547e534ea13ead11d7ebf3a29e9515424caaaf Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 25 Apr 2025 19:44:43 +0000 Subject: [PATCH 134/381] fix chat::chat_tests::test_lost_member_added --- src/chat/chat_tests.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 314a62cefc..4a6a45e71d 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -630,6 +630,7 @@ async fn test_lost_member_added() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = &tcm.alice().await; let bob = &tcm.bob().await; + let charlie = &tcm.charlie().await; let alice_chat_id = alice .create_group_with_members(ProtectionStatus::Unprotected, "Group", &[bob]) .await; @@ -638,8 +639,8 @@ async fn test_lost_member_added() -> Result<()> { assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 2); // Attempt to add member, but message is lost. - let claire_id = Contact::create(alice, "", "claire@foo.de").await?; - add_contact_to_chat(alice, alice_chat_id, claire_id).await?; + let charlie_id = alice.add_or_lookup_contact_id(charlie).await; + add_contact_to_chat(alice, alice_chat_id, charlie_id).await?; alice.pop_sent_msg().await; let alice_sent = alice.send_text(alice_chat_id, "Hi again!").await; From 92f14570f27ecc970c476f8955eb5609aed9aaeb Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 25 Apr 2025 20:09:36 +0000 Subject: [PATCH 135/381] fixup --- src/mimefactory.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 93c82acbac..095e4f82de 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -343,6 +343,10 @@ impl MimeFactory { if !fingerprint.is_empty() { past_member_fingerprints.push(fingerprint); + } else if id == ContactId::SELF { + // It's fine to have self in past members + // if we are leaving the group. + member_fingerprints.push(self_fingerprint.clone()); } else { debug_assert!(past_member_fingerprints.is_empty(), "If some past member is a PGP-contact, all other past members should be PGP-contacts too"); } From 8fc0daaffe0d57805d993616ef63a130128e41f3 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 25 Apr 2025 20:36:24 +0000 Subject: [PATCH 136/381] fixup --- src/mimefactory.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 095e4f82de..fec5a16001 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -346,7 +346,7 @@ impl MimeFactory { } else if id == ContactId::SELF { // It's fine to have self in past members // if we are leaving the group. - member_fingerprints.push(self_fingerprint.clone()); + past_member_fingerprints.push(self_fingerprint.clone()); } else { debug_assert!(past_member_fingerprints.is_empty(), "If some past member is a PGP-contact, all other past members should be PGP-contacts too"); } From 62c60dfb7e26288774d650e5c023647e72d7c762 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 25 Apr 2025 22:01:46 +0000 Subject: [PATCH 137/381] sections for test_parallel_member_remove --- src/chat/chat_tests.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 4a6a45e71d..99b6bc83d3 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -380,7 +380,7 @@ async fn test_parallel_member_remove() -> Result<()> { let alice_fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await; let alice_charlie_contact_id = alice.add_or_lookup_contact_id(&charlie).await; - // Create and promote a group. + tcm.section("Alice creates and promotes a group"); let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?; add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?; @@ -393,31 +393,31 @@ async fn test_parallel_member_remove() -> Result<()> { let bob_chat_id = bob_received_msg.get_chat_id(); bob_chat_id.accept(&bob).await?; - // Alice adds Charlie to the chat. + tcm.section("Alice adds Charlie to the chat"); add_contact_to_chat(&alice, alice_chat_id, alice_charlie_contact_id).await?; let alice_sent_add_msg = alice.pop_sent_msg().await; - // Bob leaves the chat. + tcm.section("Bob leaves the chat"); remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?; bob.pop_sent_msg().await; - // Bob receives a msg about Alice adding Claire to the group. + tcm.section("Bob receives a message about Alice adding Charlie to the group"); bob.recv_msg(&alice_sent_add_msg).await; SystemTime::shift(Duration::from_secs(3600)); - // Alice sends a message to Bob because the message about leaving is lost. + tcm.section("Alice sends a message to Bob because the message about leaving is lost"); let alice_sent_msg = alice.send_text(alice_chat_id, "What a silence!").await; bob.recv_msg(&alice_sent_msg).await; bob.golden_test_chat(bob_chat_id, "chat_test_parallel_member_remove") .await; - // Alice removes Bob from the chat. + tcm.section("Alice removes Bob from the chat"); remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?; let alice_sent_remove_msg = alice.pop_sent_msg().await; - // Bob receives a msg about Alice removing him from the group. + tcm.section("Bob receives a msg about Alice removing him from the group"); let bob_received_remove_msg = bob.recv_msg(&alice_sent_remove_msg).await; // Test that remove message is rewritten. From aed67e16e5ddcac3cb1bf7e7ffe03f87bc5e8768 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 25 Apr 2025 23:05:06 +0000 Subject: [PATCH 138/381] encrypt to removed member --- src/mimefactory.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index fec5a16001..d020147d55 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -335,6 +335,15 @@ impl MimeFactory { // we need to notify removed member // that it was removed. recipients.push(addr.clone()); + + if let Some(public_key) = public_key_opt { + keys.push((addr.clone(), public_key)) + } else if id != ContactId::SELF { + missing_key_addresses.insert(addr.clone()); + if is_encrypted { + warn!(context, "Missing key for {addr}"); + } + } } } if !undisclosed_recipients { From 4cdce20ffe003c118107f66995b7198656dd9a7d Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 26 Apr 2025 00:24:39 +0000 Subject: [PATCH 139/381] recognize self in member removed messages --- src/receive_imf.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index e50b51011e..a94b979141 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -3232,6 +3232,18 @@ async fn lookup_pgp_contact_by_address( addr: &str, chat_id: ChatId, ) -> Result> { + if context.is_self_addr(addr).await? { + let is_self_in_chat = context + .sql + .exists( + "SELECT COUNT(*) FROM chats_contacts WHERE chat_id=? AND contact_id=1", + (chat_id,), + ) + .await?; + if is_self_in_chat { + return Ok(Some(ContactId::SELF)); + } + } let contact_id: Option = context .sql .query_row_optional( From 6634dd8cf35436ed3a2a268781b39078395703e2 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 26 Apr 2025 01:05:11 +0000 Subject: [PATCH 140/381] don't warn about missing fingerprints --- src/receive_imf.rs | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index a94b979141..a1a9b4b227 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -334,19 +334,24 @@ pub(crate) async fn receive_imf_inner( let to_member_fingerprints; let past_member_fingerprints; - if member_fingerprints.len() >= mime_parser.recipients.len() { - (to_member_fingerprints, past_member_fingerprints) = - member_fingerprints.split_at(mime_parser.recipients.len()); + if !member_fingerprints.is_empty() { + if member_fingerprints.len() >= mime_parser.recipients.len() { + (to_member_fingerprints, past_member_fingerprints) = + member_fingerprints.split_at(mime_parser.recipients.len()); + } else { + warn!( + context, + "Unexpected length of the fingerprint header, expected at least {}, got {}.", + mime_parser.recipients.len(), + member_fingerprints.len() + ); + to_member_fingerprints = &[]; + past_member_fingerprints = &[]; + } } else { - warn!( - context, - "Unexpected length of the fingerprint header, expected at least {}, got {}.", - mime_parser.recipients.len(), - member_fingerprints.len() - ); to_member_fingerprints = &[]; past_member_fingerprints = &[]; - }; + } let pgp_to_ids = add_or_lookup_pgp_contacts_by_address_list( context, From 29e7bdfd231b329eff58fb2f6bc60dcc30f21f71 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 26 Apr 2025 01:29:26 +0000 Subject: [PATCH 141/381] fail if no keys are available --- src/mimefactory.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index d020147d55..8cad2e6542 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -301,7 +301,9 @@ impl MimeFactory { }; if add_timestamp >= remove_timestamp { if !recipients_contain_addr(&to, &addr) { - recipients.push(addr.clone()); + if id != ContactId::SELF { + recipients.push(addr.clone()); + } if !undisclosed_recipients { to.push((name, addr.clone())); @@ -334,7 +336,9 @@ impl MimeFactory { // This is a "member removed" message, // we need to notify removed member // that it was removed. - recipients.push(addr.clone()); + if id != ContactId::SELF { + recipients.push(addr.clone()); + } if let Some(public_key) = public_key_opt { keys.push((addr.clone(), public_key)) @@ -394,6 +398,13 @@ impl MimeFactory { encryption_keys = if !is_encrypted { None } else { + if keys.is_empty() && !recipients.is_empty() { + bail!( + "No recipient keys are available, cannot encrypt to {:?}.", + recipients + ); + } + // Remove recipients for which the key is missing. if !missing_key_addresses.is_empty() { recipients.retain(|addr| !missing_key_addresses.contains(addr)); @@ -402,6 +413,7 @@ impl MimeFactory { Some(keys) }; } + let (in_reply_to, references) = context .sql .query_row( From 0f3d605998d94b30e2c34a8dbb94fe7d8ec7b75f Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 27 Apr 2025 05:41:57 +0000 Subject: [PATCH 142/381] fix tests::verified_chats::test_verified_oneonone_chat_not_broken_by_device_change --- src/tests/verified_chats.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/verified_chats.rs b/src/tests/verified_chats.rs index 76dfbdade6..c7c0e3b41a 100644 --- a/src/tests/verified_chats.rs +++ b/src/tests/verified_chats.rs @@ -59,14 +59,14 @@ async fn check_verified_oneonone_chat_protection_not_broken(broken_by_classical_ assert_verified(&alice, &bob, ProtectionStatus::Protected).await; } else { tcm.section("Bob sets up another Delta Chat device"); - let bob2 = TestContext::new().await; + let bob2 = tcm.unconfigured().await; bob2.set_name("bob2"); bob2.configure_addr("bob@example.net").await; SystemTime::shift(std::time::Duration::from_secs(3600)); tcm.send_recv(&bob2, &alice, "Using another device now") .await; - let contact = alice.add_or_lookup_contact(&bob).await; + let contact = alice.add_or_lookup_contact(&bob2).await; assert_eq!(contact.is_verified(&alice).await.unwrap(), false); assert_verified(&alice, &bob, ProtectionStatus::Protected).await; } From 8bedaca995d48adc6a69fdb8fe30d7bb7fa64ecc Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 27 Apr 2025 06:01:50 +0000 Subject: [PATCH 143/381] do not allow setting profile image for ad hoc groups and mailing lists --- src/chat.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index c1e7da9314..6c550c4d08 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -4277,8 +4277,12 @@ pub async fn set_chat_profile_image( ensure!(!chat_id.is_special(), "Invalid chat ID"); let mut chat = Chat::load_from_db(context, chat_id).await?; ensure!( - chat.typ == Chattype::Group || chat.typ == Chattype::Mailinglist, - "Failed to set profile image; group does not exist" + chat.typ == Chattype::Group, + "Can only set profile image for group chats" + ); + ensure!( + !chat.grpid.is_empty(), + "Cannot set profile image for ad hoc groups" ); /* we should respect this - whatever we send to the group, it gets discarded anyway! */ if !is_contact_in_chat(context, chat_id, ContactId::SELF).await? { From a35bf8df02cebfc1d95580c20135d94823b28ece Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 27 Apr 2025 06:43:55 +0000 Subject: [PATCH 144/381] replace test_group_avatar_unencrypted --- src/chat/chat_tests.rs | 33 ++++++++++++++++++++++++- src/mimefactory/mimefactory_tests.rs | 37 ---------------------------- 2 files changed, 32 insertions(+), 38 deletions(-) diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 99b6bc83d3..2fbf29a69a 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -4128,7 +4128,7 @@ async fn test_no_email_contacts_in_group_chats() -> Result<()> { Ok(()) } -/// Tests that PGP-contacts cannot be added to ad-hoc groups. +/// Tests that PGP-contacts cannot be added to ad hoc groups. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_no_pgp_contacts_in_adhoc_chats() -> Result<()> { let mut tcm = TestContextManager::new(); @@ -4163,3 +4163,34 @@ async fn test_no_pgp_contacts_in_adhoc_chats() -> Result<()> { Ok(()) } + +/// Tests that avatar cannot be set in ad hoc groups. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_no_avatar_in_adhoc_chats() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + + let chat_id = receive_imf( + alice, + b"Subject: Email thread\r\n\ + From: alice@example.org\r\n\ + To: Bob , Fiona \r\n\ + Date: Mon, 2 Dec 2023 16:59:39 +0000\r\n\ + Message-ID: \r\n\ + \r\n\ + Starting a new thread\r\n", + false, + ) + .await? + .unwrap() + .chat_id; + + // Test that setting avatar in ad hoc group is not possible. + let file = alice.dir.path().join("avatar.png"); + let bytes = include_bytes!("../../test-data/image/avatar64x64.png"); + tokio::fs::write(&file, bytes).await?; + let res = set_chat_profile_image(alice, chat_id, file.to_str().unwrap()).await; + assert!(res.is_err()); + + Ok(()) +} diff --git a/src/mimefactory/mimefactory_tests.rs b/src/mimefactory/mimefactory_tests.rs index 22306ddebe..ec1bfac825 100644 --- a/src/mimefactory/mimefactory_tests.rs +++ b/src/mimefactory/mimefactory_tests.rs @@ -623,43 +623,6 @@ async fn test_selfavatar_unencrypted() -> anyhow::Result<()> { Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_group_avatar_unencrypted() -> anyhow::Result<()> { - let t = &TestContext::new_alice().await; - let group_id = chat::create_group_chat(t, chat::ProtectionStatus::Unprotected, "Group") - .await - .unwrap(); - let bob = Contact::create(t, "", "bob@example.org").await?; - chat::add_contact_to_chat(t, group_id, bob).await?; - - let file = t.dir.path().join("avatar.png"); - let bytes = include_bytes!("../../test-data/image/avatar64x64.png"); - tokio::fs::write(&file, bytes).await?; - chat::set_chat_profile_image(t, group_id, file.to_str().unwrap()).await?; - - // Send message to bob: that should get multipart/mixed because of the avatar moved to inner header. - let mut msg = Message::new_text("this is the text!".to_string()); - let sent_msg = t.send_msg(group_id, &mut msg).await; - let mut payload = sent_msg.payload().splitn(3, "\r\n\r\n"); - - let outer = payload.next().unwrap(); - let inner = payload.next().unwrap(); - let body = payload.next().unwrap(); - - assert_eq!(outer.match_indices("multipart/mixed").count(), 1); - assert_eq!(outer.match_indices("Message-ID:").count(), 1); - assert_eq!(outer.match_indices("Subject:").count(), 1); - assert_eq!(outer.match_indices("Autocrypt:").count(), 1); - assert_eq!(outer.match_indices("Chat-Group-Avatar:").count(), 0); - - assert_eq!(inner.match_indices("text/plain").count(), 1); - assert_eq!(inner.match_indices("Message-ID:").count(), 1); - assert_eq!(inner.match_indices("Chat-Group-Avatar:").count(), 1); - - assert_eq!(body.match_indices("this is the text!").count(), 1); - Ok(()) -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_selfavatar_unencrypted_signed() { // create chat with bob, set selfavatar From 23c90dfead9dc332698cfd8ac5ddc86d70bc167a Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 27 Apr 2025 17:40:26 +0000 Subject: [PATCH 145/381] fix receive_imf::receive_imf_tests::test_chat_assignment_adhoc --- src/mimefactory.rs | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 8cad2e6542..862fe3e594 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -307,12 +307,14 @@ impl MimeFactory { if !undisclosed_recipients { to.push((name, addr.clone())); - if !fingerprint.is_empty() { - member_fingerprints.push(fingerprint); - } else if id == ContactId::SELF { - member_fingerprints.push(self_fingerprint.clone()); - } else { - debug_assert!(member_fingerprints.is_empty(), "If some past member is a PGP-contact, all other past members should be PGP-contacts too"); + if is_encrypted { + if !fingerprint.is_empty() { + member_fingerprints.push(fingerprint); + } else if id == ContactId::SELF { + member_fingerprints.push(self_fingerprint.clone()); + } else { + debug_assert!(member_fingerprints.is_empty(), "If some past member is a PGP-contact, all other past members should be PGP-contacts too"); + } } member_timestamps.push(add_timestamp); } @@ -354,14 +356,16 @@ impl MimeFactory { past_members.push((name, addr.clone())); past_member_timestamps.push(remove_timestamp); - if !fingerprint.is_empty() { - past_member_fingerprints.push(fingerprint); - } else if id == ContactId::SELF { - // It's fine to have self in past members - // if we are leaving the group. - past_member_fingerprints.push(self_fingerprint.clone()); - } else { - debug_assert!(past_member_fingerprints.is_empty(), "If some past member is a PGP-contact, all other past members should be PGP-contacts too"); + if is_encrypted { + if !fingerprint.is_empty() { + past_member_fingerprints.push(fingerprint); + } else if id == ContactId::SELF { + // It's fine to have self in past members + // if we are leaving the group. + past_member_fingerprints.push(self_fingerprint.clone()); + } else { + debug_assert!(past_member_fingerprints.is_empty(), "If some past member is a PGP-contact, all other past members should be PGP-contacts too"); + } } } } @@ -375,12 +379,16 @@ impl MimeFactory { if let Some(position) = to.iter().position(|(_, x)| x == &from_addr) { to.remove(position); member_timestamps.remove(position); - member_fingerprints.remove(position); + if is_encrypted { + member_fingerprints.remove(position); + } } } member_timestamps.extend(past_member_timestamps); - member_fingerprints.extend(past_member_fingerprints); + if is_encrypted { + member_fingerprints.extend(past_member_fingerprints); + } Ok(()) }, ) From d37274d022a6e32a388b12dad7f1000aea2a5596 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 27 Apr 2025 20:04:56 +0000 Subject: [PATCH 146/381] fix test_only_minimal_data_are_forwarded --- src/chat/chat_tests.rs | 37 ++++++++++++++++++++++--------------- src/mimeparser.rs | 8 ++------ 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 2fbf29a69a..b7d049be51 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -2190,40 +2190,47 @@ async fn test_forward_group() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_only_minimal_data_are_forwarded() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + let charlie = tcm.charlie().await; + // send a message from Alice to a group with Bob - let alice = TestContext::new_alice().await; alice .set_config(Config::Displayname, Some("secretname")) .await?; - let bob_id = Contact::create(&alice, "bob", "bob@example.net").await?; + let bob_id = alice.add_or_lookup_contact_id(&bob).await; let group_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "secretgrpname").await?; add_contact_to_chat(&alice, group_id, bob_id).await?; let mut msg = Message::new_text("bla foo".to_owned()); let sent_msg = alice.send_msg(group_id, &mut msg).await; - assert!(sent_msg.payload().contains("secretgrpname")); - assert!(sent_msg.payload().contains("secretname")); - assert!(sent_msg.payload().contains("alice")); + let parsed_msg = alice.parse_msg(&sent_msg).await; + let encrypted_payload = String::from_utf8(parsed_msg.decoded_data.clone()).unwrap(); + assert!(encrypted_payload.contains("secretgrpname")); + assert!(encrypted_payload.contains("secretname")); + assert!(encrypted_payload.contains("alice")); // Bob forwards that message to Claire - // Claire should not get information about Alice for the original Group - let bob = TestContext::new_bob().await; let orig_msg = bob.recv_msg(&sent_msg).await; - let claire_id = Contact::create(&bob, "claire", "claire@foo").await?; - let single_id = ChatId::create_for_contact(&bob, claire_id).await?; + let charlie_id = bob.add_or_lookup_contact_id(&charlie).await; + let single_id = ChatId::create_for_contact(&bob, charlie_id).await?; let group_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "group2").await?; - add_contact_to_chat(&bob, group_id, claire_id).await?; + add_contact_to_chat(&bob, group_id, charlie_id).await?; let broadcast_id = create_broadcast_list(&bob).await?; - add_contact_to_chat(&bob, broadcast_id, claire_id).await?; + add_contact_to_chat(&bob, broadcast_id, charlie_id).await?; for chat_id in &[single_id, group_id, broadcast_id] { forward_msgs(&bob, &[orig_msg.id], *chat_id).await?; let sent_msg = bob.pop_sent_msg().await; - assert!(sent_msg - .payload() + let parsed_msg = bob.parse_msg(&sent_msg).await; + let encrypted_payload = String::from_utf8(parsed_msg.decoded_data.clone()).unwrap(); + + assert!(encrypted_payload .contains("---------- Forwarded message ----------")); - assert!(!sent_msg.payload().contains("secretgrpname")); - assert!(!sent_msg.payload().contains("secretname")); - assert!(!sent_msg.payload().contains("alice")); + assert!(!encrypted_payload.contains("secretgrpname")); + assert!(!encrypted_payload.contains("secretname")); + assert!(!encrypted_payload.contains("alice")); } Ok(()) diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 3eb03208a4..3c77357191 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -114,8 +114,7 @@ pub(crate) struct MimeMessage { /// MIME message in this case. pub is_mime_modified: bool, - /// Decrypted, raw MIME structure. Nonempty iff `is_mime_modified` and the message was actually - /// encrypted. + /// Decrypted raw MIME structure. pub decoded_data: Vec, /// Hop info for debugging. @@ -608,10 +607,7 @@ impl MimeMessage { parser.maybe_remove_inline_mailinglist_footer(); parser.heuristically_parse_ndn(context).await; parser.parse_headers(context).await?; - - if parser.is_mime_modified { - parser.decoded_data = mail_raw; - } + parser.decoded_data = mail_raw; Ok(parser) } From 5f0dee09ab6ced0ed1d45ff908219c770a2d0a8b Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 28 Apr 2025 00:38:24 +0000 Subject: [PATCH 147/381] fix receive_imf::receive_imf_tests::test_no_op_member_added_is_trash --- src/receive_imf/receive_imf_tests.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 9b1f7945af..1ef0b00cc6 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -4892,6 +4892,7 @@ async fn test_no_op_member_added_is_trash() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = &tcm.alice().await; let bob = &tcm.bob().await; + let fiona = &tcm.fiona().await; let alice_chat_id = alice .create_group_with_members(ProtectionStatus::Unprotected, "foos", &[bob]) .await; @@ -4901,11 +4902,11 @@ async fn test_no_op_member_added_is_trash() -> Result<()> { let bob_chat_id = bob.get_last_msg().await.chat_id; bob_chat_id.accept(bob).await?; - let fiona_id = Contact::create(alice, "", "fiona@example.net").await?; + let fiona_id = alice.add_or_lookup_contact_id(fiona).await; add_contact_to_chat(alice, alice_chat_id, fiona_id).await?; let msg = alice.pop_sent_msg().await; - let fiona_id = Contact::create(bob, "", "fiona@example.net").await?; + let fiona_id = bob.add_or_lookup_contact_id(fiona).await; add_contact_to_chat(bob, bob_chat_id, fiona_id).await?; bob.recv_msg_trash(&msg).await; let contacts = get_chat_contacts(bob, bob_chat_id).await?; From 570b7679027714d08e040016b611e1325b44f450 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 29 Apr 2025 01:17:46 +0000 Subject: [PATCH 148/381] assign incomplete messages to whatever PGP chat --- src/chat/chat_tests.rs | 3 +- src/imap.rs | 4 +- src/message/message_tests.rs | 6 ++- src/receive_imf.rs | 73 +++++++++++++++++++++++++++++------- 4 files changed, 67 insertions(+), 19 deletions(-) diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index b7d049be51..900bcc0173 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -2226,8 +2226,7 @@ async fn test_only_minimal_data_are_forwarded() -> Result<()> { let parsed_msg = bob.parse_msg(&sent_msg).await; let encrypted_payload = String::from_utf8(parsed_msg.decoded_data.clone()).unwrap(); - assert!(encrypted_payload - .contains("---------- Forwarded message ----------")); + assert!(encrypted_payload.contains("---------- Forwarded message ----------")); assert!(!encrypted_payload.contains("secretgrpname")); assert!(!encrypted_payload.contains("secretname")); assert!(!encrypted_payload.contains("alice")); diff --git a/src/imap.rs b/src/imap.rs index 1dd934ddcf..e8c6549fd0 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -1886,7 +1886,7 @@ async fn should_move_out_of_spam( }; // No chat found. let (from_id, blocked_contact, _origin) = - match from_field_to_contact_id(context, &from, None, true) + match from_field_to_contact_id(context, &from, None, true, false) .await .context("from_field_to_contact_id")? { @@ -2244,7 +2244,7 @@ pub(crate) async fn prefetch_should_download( None => return Ok(false), }; let (_from_id, blocked_contact, origin) = - match from_field_to_contact_id(context, &from, None, true).await? { + match from_field_to_contact_id(context, &from, None, true, true).await? { Some(res) => res, None => return Ok(false), }; diff --git a/src/message/message_tests.rs b/src/message/message_tests.rs index 9c82a2b652..d51a723dfc 100644 --- a/src/message/message_tests.rs +++ b/src/message/message_tests.rs @@ -410,12 +410,16 @@ async fn test_markseen_not_downloaded_msg() -> Result<()> { let alice = &tcm.alice().await; alice.set_config(Config::DownloadLimit, Some("1")).await?; let bob = &tcm.bob().await; - let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id; + let bob_chat_id = bob.create_chat(alice).await.id; + alice.create_chat(bob).await; // Make sure the chat is accepted. + tcm.section("Bob sends a large message to Alice"); let file_bytes = include_bytes!("../../test-data/image/screenshot.png"); let mut msg = Message::new(Viewtype::Image); msg.set_file_from_bytes(bob, "a.jpg", file_bytes, None)?; let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await; + + tcm.section("Alice receives a large message from Bob"); let msg = alice.recv_msg(&sent_msg).await; assert_eq!(msg.download_state, DownloadState::Available); assert!(!msg.param.get_bool(Param::WantsMdn).unwrap_or_default()); diff --git a/src/receive_imf.rs b/src/receive_imf.rs index a1a9b4b227..62a37705df 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -6,7 +6,9 @@ use std::sync::LazyLock; use anyhow::{Context as _, Result}; use data_encoding::BASE32_NOPAD; -use deltachat_contact_tools::{addr_cmp, may_be_valid_addr, sanitize_single_line, ContactAddress}; +use deltachat_contact_tools::{ + addr_cmp, addr_normalize, may_be_valid_addr, sanitize_single_line, ContactAddress, +}; use iroh_gossip::proto::TopicId; use mailparse::SingleInfo; use num_traits::FromPrimitive; @@ -301,19 +303,24 @@ pub(crate) async fn receive_imf_inner( // but uses display name of the user whose action generated the notification // as the display name. let fingerprint = mime_parser.signatures.iter().next(); - let (from_id, _from_id_blocked, incoming_origin) = - match from_field_to_contact_id(context, &mime_parser.from, fingerprint, prevent_rename) - .await? - { - Some(contact_id_res) => contact_id_res, - None => { - warn!( - context, - "receive_imf: From field does not contain an acceptable address." - ); - return Ok(None); - } - }; + let (from_id, _from_id_blocked, incoming_origin) = match from_field_to_contact_id( + context, + &mime_parser.from, + fingerprint, + prevent_rename, + is_partial_download.is_some(), + ) + .await? + { + Some(contact_id_res) => contact_id_res, + None => { + warn!( + context, + "receive_imf: From field does not contain an acceptable address." + ); + return Ok(None); + } + }; let chat_id = if let Some(grpid) = mime_parser.get_chat_group_id() { if let Some((chat_id, _protected, _blocked)) = @@ -693,12 +700,19 @@ pub(crate) async fn receive_imf_inner( /// display names. We don't want the display name to change every time the user gets a new email from /// a mailing list. /// +/// * `is_partial_download`: the message is partially downloaded. +/// We only know the email address and not the contact fingerprint, +/// but try to assign the message to some PGP-contact. +/// If we get it wrong, the message will be placed into the correct +/// chat after downloading. +/// /// Returns `None` if From field does not contain a valid contact address. pub async fn from_field_to_contact_id( context: &Context, from: &SingleInfo, fingerprint: Option<&Fingerprint>, prevent_rename: bool, + is_partial_download: bool, ) -> Result> { let fingerprint = fingerprint.as_ref().map(|fp| fp.hex()).unwrap_or_default(); let display_name = if prevent_rename { @@ -717,6 +731,37 @@ pub async fn from_field_to_contact_id( } }; + if fingerprint.is_empty() && is_partial_download { + let addr_normalized = addr_normalize(&from_addr); + + // Try to assign to some PGP-contact. + if let Some((from_id, origin)) = context + .sql + .query_row_optional( + "SELECT id, origin FROM contacts + WHERE addr=?1 COLLATE NOCASE + AND fingerprint<>'' -- Only PGP-contacts + AND id>?2 AND origin>=?3 AND blocked=?4 + ORDER BY last_seen DESC + LIMIT 1", + ( + &addr_normalized, + ContactId::LAST_SPECIAL, + Origin::IncomingUnknownFrom, + Blocked::Not, + ), + |row| { + let id: ContactId = row.get(0)?; + let origin: Origin = row.get(1)?; + Ok((id, origin)) + }, + ) + .await? + { + return Ok(Some((from_id, false, origin))); + } + } + let (from_id, _) = Contact::add_or_lookup_ex( context, display_name.unwrap_or_default(), From e4648e90c69655aae6866fb48d20dfe2dd86ee1c Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 29 Apr 2025 22:47:24 +0000 Subject: [PATCH 149/381] fix receive_imf::receive_imf_tests::test_create_group_with_big_msg --- src/receive_imf/receive_imf_tests.rs | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 1ef0b00cc6..04f72aa823 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -4566,12 +4566,9 @@ async fn test_create_group_with_big_msg() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = tcm.alice().await; let bob = tcm.bob().await; - let ba_contact = Contact::create( - &bob, - "alice", - &alice.get_config(Config::Addr).await?.unwrap(), - ) - .await?; + let ba_contact = bob.add_or_lookup_contact_id(&alice).await; + let ab_chat_id = alice.create_chat(&bob).await.id; + let file_bytes = include_bytes!("../../test-data/image/screenshot.png"); let bob_grp_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "Group").await?; @@ -4579,26 +4576,22 @@ async fn test_create_group_with_big_msg() -> Result<()> { let mut msg = Message::new(Viewtype::Image); msg.set_file_from_bytes(&bob, "a.jpg", file_bytes, None)?; let sent_msg = bob.send_msg(bob_grp_id, &mut msg).await; - assert!(!msg.get_showpadlock()); + assert!(msg.get_showpadlock()); alice.set_config(Config::DownloadLimit, Some("1")).await?; assert_eq!(alice.download_limit().await?, Some(MIN_DOWNLOAD_LIMIT)); let msg = alice.recv_msg(&sent_msg).await; assert_eq!(msg.download_state, DownloadState::Available); - let alice_grp = Chat::load_from_db(&alice, msg.chat_id).await?; - assert_eq!(alice_grp.typ, Chattype::Group); - assert_eq!(alice_grp.name, "Group"); - assert_eq!( - chat::get_chat_contacts(&alice, alice_grp.id).await?.len(), - 2 - ); + let alice_chat = Chat::load_from_db(&alice, msg.chat_id).await?; + // Incomplete message is assigned to 1:1 chat. + assert_eq!(alice_chat.typ, Chattype::Single); alice.set_config(Config::DownloadLimit, None).await?; let msg = alice.recv_msg(&sent_msg).await; assert_eq!(msg.download_state, DownloadState::Done); assert_eq!(msg.state, MessageState::InFresh); assert_eq!(msg.viewtype, Viewtype::Image); - assert_eq!(msg.chat_id, alice_grp.id); + assert_ne!(msg.chat_id, alice_chat.id); let alice_grp = Chat::load_from_db(&alice, msg.chat_id).await?; assert_eq!(alice_grp.typ, Chattype::Group); assert_eq!(alice_grp.name, "Group"); @@ -4607,7 +4600,6 @@ async fn test_create_group_with_big_msg() -> Result<()> { 2 ); - let ab_chat_id = tcm.send_recv_accept(&alice, &bob, "hi").await.chat_id; // Now Bob can send encrypted messages to Alice. let bob_grp_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "Group1").await?; @@ -4638,7 +4630,8 @@ async fn test_create_group_with_big_msg() -> Result<()> { ); // The big message must go away from the 1:1 chat. - assert_eq!(alice.get_last_msg_in(ab_chat_id).await.text, "hi"); + let msgs = chat::get_chat_msgs(&alice, ab_chat_id).await?; + assert!(msgs.is_empty()); Ok(()) } From 5b12fd3af42659b6c3b0e5fce70567f7e2463e29 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 06:06:27 +0000 Subject: [PATCH 150/381] clippy --- src/chat.rs | 2 +- src/chat/chat_tests.rs | 28 ++++++++++---------- src/contact/contact_tests.rs | 22 ++++++++-------- src/imex/key_transfer.rs | 12 +++------ src/message/message_tests.rs | 1 - src/receive_imf.rs | 23 ++++++++--------- src/receive_imf/receive_imf_tests.rs | 2 -- src/securejoin.rs | 1 - src/securejoin/securejoin_tests.rs | 8 +++--- src/stock_str.rs | 5 ---- src/tests/aeap.rs | 38 +++++++--------------------- 11 files changed, 53 insertions(+), 89 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index 6c550c4d08..a36d677955 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -4897,7 +4897,7 @@ async fn set_contacts_by_fingerprints( for (fingerprint, addr) in fingerprint_addrs { let contact_addr = ContactAddress::new(addr)?; let contact = - Contact::add_or_lookup_ex(context, "", &contact_addr, &fingerprint, Origin::Hidden) + Contact::add_or_lookup_ex(context, "", &contact_addr, fingerprint, Origin::Hidden) .await? .0; contacts.insert(contact); diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 900bcc0173..efc41f54ef 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -2695,7 +2695,7 @@ async fn test_chat_get_encryption_info() -> Result<()> { add_contact_to_chat(alice, chat_id, contact_bob).await?; assert_eq!( - chat_id.get_encryption_info(&alice).await?, + chat_id.get_encryption_info(alice).await?, "End-to-end encryption available\n\ \n\ bob@example.net\n\ @@ -2703,9 +2703,9 @@ async fn test_chat_get_encryption_info() -> Result<()> { 65F1 DB18 B18C BCF7 0487" ); - add_contact_to_chat(&alice, chat_id, contact_fiona).await?; + add_contact_to_chat(alice, chat_id, contact_fiona).await?; assert_eq!( - chat_id.get_encryption_info(&alice).await?, + chat_id.get_encryption_info(alice).await?, "End-to-end encryption available\n\ \n\ fiona@example.net\n\ @@ -2719,7 +2719,7 @@ async fn test_chat_get_encryption_info() -> Result<()> { let email_chat = alice.create_email_chat(bob).await; assert_eq!( - email_chat.id.get_encryption_info(&alice).await?, + email_chat.id.get_encryption_info(alice).await?, "No encryption" ); @@ -2933,29 +2933,29 @@ async fn test_sync_blocked() -> Result<()> { let sent_msg = bob.send_text(ba_chat.id, "hi").await; let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id; alice1.recv_msg(&sent_msg).await; - let a0b_contact_id = alice0.add_or_lookup_contact_id(&bob).await; + let a0b_contact_id = alice0.add_or_lookup_contact_id(bob).await; - assert_eq!(alice1.get_pgp_chat(&bob).await.blocked, Blocked::Request); + assert_eq!(alice1.get_pgp_chat(bob).await.blocked, Blocked::Request); a0b_chat_id.accept(alice0).await?; sync(alice0, alice1).await; - assert_eq!(alice1.get_pgp_chat(&bob).await.blocked, Blocked::Not); + assert_eq!(alice1.get_pgp_chat(bob).await.blocked, Blocked::Not); a0b_chat_id.block(alice0).await?; sync(alice0, alice1).await; - assert_eq!(alice1.get_pgp_chat(&bob).await.blocked, Blocked::Yes); + assert_eq!(alice1.get_pgp_chat(bob).await.blocked, Blocked::Yes); a0b_chat_id.unblock(alice0).await?; sync(alice0, alice1).await; - assert_eq!(alice1.get_pgp_chat(&bob).await.blocked, Blocked::Not); + assert_eq!(alice1.get_pgp_chat(bob).await.blocked, Blocked::Not); // Unblocking a 1:1 chat doesn't unblock the contact currently. Contact::unblock(alice0, a0b_contact_id).await?; - assert!(!alice1.add_or_lookup_contact(&bob).await.is_blocked()); + assert!(!alice1.add_or_lookup_contact(bob).await.is_blocked()); Contact::block(alice0, a0b_contact_id).await?; sync(alice0, alice1).await; - assert!(alice1.add_or_lookup_contact(&bob).await.is_blocked()); + assert!(alice1.add_or_lookup_contact(bob).await.is_blocked()); Contact::unblock(alice0, a0b_contact_id).await?; sync(alice0, alice1).await; - assert!(!alice1.add_or_lookup_contact(&bob).await.is_blocked()); + assert!(!alice1.add_or_lookup_contact(bob).await.is_blocked()); // Test accepting and blocking groups. This way we test: // - Group chats synchronisation. @@ -3262,7 +3262,7 @@ async fn test_sync_broadcast() -> Result<()> { a.set_config_bool(Config::SyncMsgs, true).await?; } let bob = &tcm.bob().await; - let a0b_contact_id = alice0.add_or_lookup_contact(&bob).await.id; + let a0b_contact_id = alice0.add_or_lookup_contact(bob).await.id; let a0_broadcast_id = create_broadcast_list(alice0).await?; sync(alice0, alice1).await; @@ -3289,7 +3289,7 @@ async fn test_sync_broadcast() -> Result<()> { ); let sent_msg = alice1.send_text(a1_broadcast_id, "hi").await; let msg = bob.recv_msg(&sent_msg).await; - let chat = Chat::load_from_db(&bob, msg.chat_id).await?; + let chat = Chat::load_from_db(bob, msg.chat_id).await?; assert_eq!(chat.get_type(), Chattype::Mailinglist); let msg = alice0.recv_msg(&sent_msg).await; assert_eq!(msg.chat_id, a0_broadcast_id); diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs index 98d41254ee..361af4bf21 100644 --- a/src/contact/contact_tests.rs +++ b/src/contact/contact_tests.rs @@ -733,20 +733,20 @@ async fn test_contact_get_encrinfo() -> Result<()> { let bob = &tcm.bob().await; // Return error for special IDs - let encrinfo = Contact::get_encrinfo(&alice, ContactId::SELF).await; + let encrinfo = Contact::get_encrinfo(alice, ContactId::SELF).await; assert!(encrinfo.is_err()); - let encrinfo = Contact::get_encrinfo(&alice, ContactId::DEVICE).await; + let encrinfo = Contact::get_encrinfo(alice, ContactId::DEVICE).await; assert!(encrinfo.is_err()); let email_contact_bob_id = alice.add_or_lookup_email_contact_id(bob).await; - let encrinfo = Contact::get_encrinfo(&alice, email_contact_bob_id).await?; + let encrinfo = Contact::get_encrinfo(alice, email_contact_bob_id).await?; assert_eq!(encrinfo, "No encryption"); - let contact = Contact::get_by_id(&alice, email_contact_bob_id).await?; - assert!(!contact.e2ee_avail(&alice).await?); + let contact = Contact::get_by_id(alice, email_contact_bob_id).await?; + assert!(!contact.e2ee_avail(alice).await?); let contact_bob_id = alice.add_or_lookup_contact_id(bob).await; - let encrinfo = Contact::get_encrinfo(&alice, contact_bob_id).await?; + let encrinfo = Contact::get_encrinfo(alice, contact_bob_id).await?; assert_eq!( encrinfo, "End-to-end encryption available. @@ -760,8 +760,8 @@ bob@example.net (bob@example.net): CCCB 5AA9 F6E1 141C 9431 65F1 DB18 B18C BCF7 0487" ); - let contact = Contact::get_by_id(&alice, contact_bob_id).await?; - assert!(contact.e2ee_avail(&alice).await?); + let contact = Contact::get_by_id(alice, contact_bob_id).await?; + assert!(contact.e2ee_avail(alice).await?); Ok(()) } @@ -786,7 +786,7 @@ async fn test_synchronize_status() -> Result<()> { let chat = alice1.create_email_chat(bob).await; // Alice sends a message to Bob from the first device. - send_text_msg(&alice1, chat.id, "Hello".to_string()).await?; + send_text_msg(alice1, chat.id, "Hello".to_string()).await?; let sent_msg = alice1.pop_sent_msg().await; // Message is not encrypted. @@ -804,7 +804,7 @@ async fn test_synchronize_status() -> Result<()> { // Alice sends encrypted message. let chat = alice1.create_chat(bob).await; - send_text_msg(&alice1, chat.id, "Hello".to_string()).await?; + send_text_msg(alice1, chat.id, "Hello".to_string()).await?; let sent_msg = alice1.pop_sent_msg().await; // Second message is encrypted. @@ -850,7 +850,7 @@ async fn test_selfavatar_changed_event() -> Result<()> { // Alice sends a message. let alice1_chat_id = alice1.create_chat(bob).await.id; - send_text_msg(&alice1, alice1_chat_id, "Hello".to_string()).await?; + send_text_msg(alice1, alice1_chat_id, "Hello".to_string()).await?; let sent_msg = alice1.pop_sent_msg().await; // The message is encrypted. diff --git a/src/imex/key_transfer.rs b/src/imex/key_transfer.rs index 7feed5448d..dbb54d5605 100644 --- a/src/imex/key_transfer.rs +++ b/src/imex/key_transfer.rs @@ -310,20 +310,14 @@ mod tests { alice2.recv_msg(&sent).await; let msg = alice2.get_last_msg().await; assert!(msg.is_setupmessage()); - assert_eq!( - crate::key::load_self_secret_keyring(&alice2).await?.len(), - 0 - ); + assert_eq!(crate::key::load_self_secret_keyring(alice2).await?.len(), 0); // Transfer the key. tcm.section("Alice imports a key from Autocrypt Setup Message"); alice2.set_config(Config::BccSelf, Some("0")).await?; - continue_key_transfer(&alice2, msg.id, &setup_code).await?; + continue_key_transfer(alice2, msg.id, &setup_code).await?; assert_eq!(alice2.get_config_bool(Config::BccSelf).await?, true); - assert_eq!( - crate::key::load_self_secret_keyring(&alice2).await?.len(), - 1 - ); + assert_eq!(crate::key::load_self_secret_keyring(alice2).await?.len(), 1); // Alice sends a message to self from the new device. let sent = alice2.send_text(msg.chat_id, "Test").await; diff --git a/src/message/message_tests.rs b/src/message/message_tests.rs index d51a723dfc..5846012bc3 100644 --- a/src/message/message_tests.rs +++ b/src/message/message_tests.rs @@ -197,7 +197,6 @@ async fn test_unencrypted_quote_encrypted_message() -> Result<()> { let bob_received_message = bob.recv_msg(&sent).await; assert_eq!(bob_received_message.get_showpadlock(), true); - let bob_chat_id = bob_received_message.chat_id; // Bob quotes encrypted message in unencrypted chat. let bob_email_chat = bob.create_email_chat(alice).await; diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 62a37705df..3d317e717a 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -324,7 +324,7 @@ pub(crate) async fn receive_imf_inner( let chat_id = if let Some(grpid) = mime_parser.get_chat_group_id() { if let Some((chat_id, _protected, _blocked)) = - chat::get_chat_id_by_grpid(context, &grpid).await? + chat::get_chat_id_by_grpid(context, grpid).await? { Some(chat_id) } else { @@ -376,7 +376,7 @@ pub(crate) async fn receive_imf_inner( past_ids = lookup_pgp_contacts_by_address_list( context, &mime_parser.past_members, - &past_member_fingerprints, + past_member_fingerprints, chat_id, ) .await?; @@ -385,7 +385,7 @@ pub(crate) async fn receive_imf_inner( context, &mime_parser.past_members, &mime_parser.gossiped_keys, - &past_member_fingerprints, + past_member_fingerprints, Origin::Hidden, ) .await?; @@ -393,7 +393,7 @@ pub(crate) async fn receive_imf_inner( } else { if pgp_to_ids.len() == 1 && pgp_to_ids - .get(0) + .first() .is_some_and(|contact_id| contact_id.is_some()) { // There is a single recipient and we have @@ -425,18 +425,17 @@ pub(crate) async fn receive_imf_inner( let received_msg; if mime_parser.get_header(HeaderDef::SecureJoin).is_some() { - let res; - if mime_parser.incoming { - res = handle_securejoin_handshake(context, &mut mime_parser, from_id) + let res = if mime_parser.incoming { + handle_securejoin_handshake(context, &mut mime_parser, from_id) .await - .context("error in Secure-Join message handling")?; + .context("error in Secure-Join message handling")? } else { let to_id = to_ids.first().copied().flatten().unwrap_or(ContactId::SELF); // handshake may mark contacts as verified and must be processed before chats are created - res = observe_securejoin_on_other_device(context, &mime_parser, to_id) + observe_securejoin_on_other_device(context, &mime_parser, to_id) .await .context("error in Secure-Join watching")? - } + }; match res { securejoin::HandshakeMessage::Done | securejoin::HandshakeMessage::Ignore => { @@ -964,7 +963,7 @@ async fn add_parts( context, mime_parser, &parent, - &to_ids, + to_ids, from_id, allow_creation || test_normal_chat.is_some(), create_blocked, @@ -2622,7 +2621,7 @@ async fn apply_group_changes( context, chat_id, Some(from_id), - &to_ids, + to_ids, past_ids, chat_group_member_timestamps, ) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 04f72aa823..8bd7ac7acf 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -4722,7 +4722,6 @@ async fn test_protected_group_add_remove_member_missing_key() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = &tcm.alice().await; let bob = &tcm.bob().await; - let bob_addr = bob.get_config(Config::Addr).await?.unwrap(); mark_as_verified(alice, bob).await; let group_id = create_group_chat(alice, ProtectionStatus::Protected, "Group").await?; let alice_bob_id = alice.add_or_lookup_contact(bob).await.id; @@ -4731,7 +4730,6 @@ async fn test_protected_group_add_remove_member_missing_key() -> Result<()> { alice.sql.execute("DELETE FROM public_keys", ()).await?; let fiona = &tcm.fiona().await; - let fiona_addr = fiona.get_config(Config::Addr).await?.unwrap(); mark_as_verified(alice, fiona).await; let alice_fiona_id = alice.add_or_lookup_contact(fiona).await.id; add_contact_to_chat(alice, group_id, alice_fiona_id).await?; diff --git a/src/securejoin.rs b/src/securejoin.rs index 574908634b..8665650fca 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -20,7 +20,6 @@ use crate::mimeparser::{MimeMessage, SystemMessage}; use crate::param::Param; use crate::qr::check_qr; use crate::securejoin::bob::JoinerProgress; -use crate::stock_str; use crate::sync::Sync::*; use crate::token; diff --git a/src/securejoin/securejoin_tests.rs b/src/securejoin/securejoin_tests.rs index 74de843932..97b3dd362b 100644 --- a/src/securejoin/securejoin_tests.rs +++ b/src/securejoin/securejoin_tests.rs @@ -350,10 +350,10 @@ async fn test_setup_contact_bob_knows_alice() -> Result<()> { tcm.section("Step 1: Generate QR-code"); // `None` indicates setup-contact. - let qr = get_securejoin_qr(&alice, None).await?; + let qr = get_securejoin_qr(alice, None).await?; tcm.section("Step 2+4: Bob scans QR-code, sends vc-request-with-auth, skipping vc-request"); - join_securejoin(&bob, &qr).await.unwrap(); + join_securejoin(bob, &qr).await.unwrap(); // Check Bob emitted the JoinerProgress event. let event = bob @@ -388,11 +388,11 @@ async fn test_setup_contact_bob_knows_alice() -> Result<()> { // Alice should not yet have Bob verified let contact_bob = alice.add_or_lookup_pgp_contact(bob).await; - assert_eq!(contact_bob.is_verified(&alice).await?, false); + assert_eq!(contact_bob.is_verified(alice).await?, false); tcm.section("Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm"); alice.recv_msg_trash(&sent).await; - assert_eq!(contact_bob.is_verified(&alice).await?, true); + assert_eq!(contact_bob.is_verified(alice).await?, true); let sent = alice.pop_sent_msg().await; let msg = bob.parse_msg(&sent).await; diff --git a/src/stock_str.rs b/src/stock_str.rs index b4ca59c633..dc8a0ab885 100644 --- a/src/stock_str.rs +++ b/src/stock_str.rs @@ -778,11 +778,6 @@ pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId } } -/// Stock string: `End-to-end encryption preferred.`. -pub(crate) async fn e2e_preferred(context: &Context) -> String { - translated(context, StockMessage::E2ePreferred).await -} - /// Stock string: `%1$s invited you to join this group. Waiting for the device of %2$s to reply…`. pub(crate) async fn secure_join_started( context: &Context, diff --git a/src/tests/aeap.rs b/src/tests/aeap.rs index 653ae5ba81..6883d4c945 100644 --- a/src/tests/aeap.rs +++ b/src/tests/aeap.rs @@ -81,29 +81,29 @@ async fn check_aeap_transition(chat_for_transition: ChatForTransition, verified: } let mut groups = vec![ - chat::create_group_chat(&bob, chat::ProtectionStatus::Unprotected, "Group 0") + chat::create_group_chat(bob, chat::ProtectionStatus::Unprotected, "Group 0") .await .unwrap(), - chat::create_group_chat(&bob, chat::ProtectionStatus::Unprotected, "Group 1") + chat::create_group_chat(bob, chat::ProtectionStatus::Unprotected, "Group 1") .await .unwrap(), ]; if verified { groups.push( - chat::create_group_chat(&bob, chat::ProtectionStatus::Protected, "Group 2") + chat::create_group_chat(bob, chat::ProtectionStatus::Protected, "Group 2") .await .unwrap(), ); groups.push( - chat::create_group_chat(&bob, chat::ProtectionStatus::Protected, "Group 3") + chat::create_group_chat(bob, chat::ProtectionStatus::Protected, "Group 3") .await .unwrap(), ); } - let alice_contact = bob.add_or_lookup_contact_id(&alice).await; + let alice_contact = bob.add_or_lookup_contact_id(alice).await; for group in &groups { - chat::add_contact_to_chat(&bob, *group, alice_contact) + chat::add_contact_to_chat(bob, *group, alice_contact) .await .unwrap(); } @@ -121,12 +121,12 @@ async fn check_aeap_transition(chat_for_transition: ChatForTransition, verified: group3_alice = Some(alice.recv_msg(&sent).await.chat_id); } - tcm.change_addr(&alice, ALICE_NEW_ADDR).await; + tcm.change_addr(alice, ALICE_NEW_ADDR).await; tcm.section("Alice sends another message to Bob, this time from her new addr"); // No matter which chat Alice sends to, the transition should be done in all groups let chat_to_send = match chat_for_transition { - OneToOne => alice.create_chat(&bob).await.id, + OneToOne => alice.create_chat(bob).await.id, GroupChat => group1_alice, VerifiedGroup => group3_alice.expect("No verified group"), }; @@ -134,14 +134,13 @@ async fn check_aeap_transition(chat_for_transition: ChatForTransition, verified: .send_text(chat_to_send, "Hello from my new addr!") .await; let recvd = bob.recv_msg(&sent).await; - let sent_timestamp = recvd.timestamp_sent; assert_eq!(recvd.text, "Hello from my new addr!"); tcm.section("Check that the AEAP transition worked"); check_that_transition_worked(bob, &groups, alice_contact, ALICE_NEW_ADDR).await; tcm.section("Test switching back"); - tcm.change_addr(&alice, "alice@example.org").await; + tcm.change_addr(alice, "alice@example.org").await; let sent = alice .send_text(chat_to_send, "Hello from my old addr!") .await; @@ -181,25 +180,6 @@ async fn check_that_transition_worked( assert_eq!(alice_contact.get_addr(), alice_addr); } -async fn get_last_info_msg(t: &TestContext, chat_id: ChatId) -> Option { - let msgs = chat::get_chat_msgs_ex( - &t.ctx, - chat_id, - chat::MessageListOptions { - info_only: true, - add_daymarker: false, - }, - ) - .await - .unwrap(); - let msg_id = if let chat::ChatItem::Message { msg_id } = msgs.last()? { - msg_id - } else { - return None; - }; - Some(Message::load_from_db(&t.ctx, *msg_id).await.unwrap()) -} - /// Test that an attacker - here Fiona - can't replay a message sent by Alice /// to make Bob think that there was a transition to Fiona's address. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] From 15256c95e06b2699b3be7d2934f39314fcf7d072 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 11:56:52 +0000 Subject: [PATCH 151/381] ignore unencrypted chat-group-id --- src/mimeparser.rs | 10 ++++ src/receive_imf/receive_imf_tests.rs | 73 ++++++++++++---------------- 2 files changed, 42 insertions(+), 41 deletions(-) diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 3c77357191..ecdcf2d41c 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -1527,6 +1527,16 @@ impl MimeMessage { remove_header(headers, "chat-verified", removed); remove_header(headers, "autocrypt-gossip", removed); + // Chat-Group-ID can only appear in encrypted messages + // since PGP-contact migration. + // + // Unencrypted ad hoc groups do not have group IDs. + // + // If we receive a Chat-Group-ID header in unencrypted message, + // it is likely sent by old version in opportunistically + // encrypted group that dropped to unencrypted. + remove_header(headers, "chat-group-id", removed); + // Secure-Join is secured unless it is an initial "vc-request"/"vg-request". if let Some(secure_join) = remove_header(headers, "secure-join", removed) { if secure_join == "vc-request" || secure_join == "vg-request" { diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 8bd7ac7acf..b457070a78 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -3762,15 +3762,15 @@ async fn test_mua_user_adds_member() -> Result<()> { receive_imf( &t, b"From: alice@example.org\n\ - To: bob@example.com\n\ - Subject: foo\n\ - Message-ID: \n\ - Chat-Version: 1.0\n\ - Chat-Group-ID: gggroupiddd\n\ - Chat-Group-Name: foo\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", + To: bob@example.com\n\ + Subject: foo\n\ + Message-ID: \n\ + Chat-Version: 1.0\n\ + Chat-Group-ID: gggroupiddd\n\ + Chat-Group-Name: foo\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", false, ) .await? @@ -3779,13 +3779,13 @@ async fn test_mua_user_adds_member() -> Result<()> { receive_imf( &t, b"From: bob@example.com\n\ - To: alice@example.org, fiona@example.net\n\ - Subject: foo\n\ - Message-ID: \n\ - In-Reply-To: Gr.gggroupiddd.12345678901@example.com\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", + To: alice@example.org, fiona@example.net\n\ + Subject: foo\n\ + Message-ID: \n\ + In-Reply-To: Gr.gggroupiddd.12345678901@example.com\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", false, ) .await? @@ -5155,8 +5155,12 @@ async fn test_make_n_send_vcard() -> Result<()> { Ok(()) } +/// Tests that group is not created if the message +/// has no recipients even if it has unencrypted Chat-Group-ID. +/// +/// Chat-Group-ID in unencrypted messages should be ignored. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_group_no_recipients() -> Result<()> { +async fn test_unencrypted_group_id_no_recipients() -> Result<()> { let t = &TestContext::new_alice().await; let raw = "From: alice@example.org Subject: Group @@ -5171,7 +5175,7 @@ Hello!" let received = receive_imf(t, raw, false).await?.unwrap(); let msg = Message::load_from_db(t, *received.msg_ids.last().unwrap()).await?; let chat = Chat::load_from_db(t, msg.chat_id).await?; - assert_eq!(chat.typ, Chattype::Group); + assert_eq!(chat.typ, Chattype::Single); // Check that the weird group name is sanitzied correctly: let mail = mailparse::parse_mail(raw).unwrap(); @@ -5189,31 +5193,18 @@ Hello!" #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_group_name_with_newline() -> Result<()> { - let t = &TestContext::new_alice().await; - let raw = "From: alice@example.org -Subject: Group -Chat-Version: 1.0 -Chat-Group-Name: =?utf-8?q?Delta=0D=0AChat?= -Chat-Group-ID: GePFDkwEj2K -Message-ID: + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; -Hello!" - .as_bytes(); - let received = receive_imf(t, raw, false).await?.unwrap(); - let msg = Message::load_from_db(t, *received.msg_ids.last().unwrap()).await?; - let chat = Chat::load_from_db(t, msg.chat_id).await?; - assert_eq!(chat.typ, Chattype::Group); + let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group\r\nwith\nnewlines").await?; + add_contact_to_chat(alice, chat_id, alice.add_or_lookup_contact_id(bob).await).await?; + send_text_msg(alice, chat_id, "populate".to_string()).await?; + let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id; - // Check that the weird group name is sanitzied correctly: - let mail = mailparse::parse_mail(raw).unwrap(); - assert_eq!( - mail.headers - .get_header(HeaderDef::ChatGroupName) - .unwrap() - .get_value(), - "Delta\r\nChat" - ); - assert_eq!(chat.name, "Delta Chat"); + let chat = Chat::load_from_db(bob, bob_chat_id).await?; + assert_eq!(chat.typ, Chattype::Group); + assert_eq!(chat.name, "Group with newlines"); Ok(()) } From dec8cf9a2af1e943dd0d6d3fa5eb577dfa9c5940 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 12:09:11 +0000 Subject: [PATCH 152/381] fix receive_imf::receive_imf_tests::test_chat_assignment_private_chat_reply --- src/receive_imf/receive_imf_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index b457070a78..b0eac2e0cb 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -2564,7 +2564,7 @@ async fn test_chat_assignment_private_chat_reply() { Subject: =?utf-8?q?single_reply-to?= {} Date: Fri, 28 May 2021 10:15:05 +0000 -To: Bob {} +To: Bob , Charlie {} From: Alice Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no Content-Transfer-Encoding: quoted-printable From e931a053b58d943fe90a0318cb11853dbf4a2152 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 12:12:25 +0000 Subject: [PATCH 153/381] remove test_read_receipt_and_unarchive --- src/receive_imf/receive_imf_tests.rs | 116 --------------------------- 1 file changed, 116 deletions(-) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index b0eac2e0cb..d823f06d91 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -271,122 +271,6 @@ async fn test_adhoc_groups_merge() -> Result<()> { Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_read_receipt_and_unarchive() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = &tcm.alice().await; - let bob = &tcm.bob().await; - - let bob_id = alice.add_or_lookup_contact_id(bob).await; - let one2one = alice.create_chat(bob).await; - one2one - .id - .set_visibility(alice, ChatVisibility::Archived) - .await - .unwrap(); - let one2one = Chat::load_from_db(alice, one2one.id).await?; - assert_eq!(one2one.get_visibility(), ChatVisibility::Archived); - - // create a group with bob, archive group - let group_id = chat::create_group_chat(alice, ProtectionStatus::Unprotected, "foo").await?; - chat::add_contact_to_chat(alice, group_id, bob_id).await?; - assert_eq!(chat::get_chat_msgs(alice, group_id).await.unwrap().len(), 0); - group_id - .set_visibility(alice, ChatVisibility::Archived) - .await?; - let group = Chat::load_from_db(alice, group_id).await?; - assert_eq!(group.get_visibility(), ChatVisibility::Archived); - - // everything archived, chatlist should be empty - assert_eq!( - Chatlist::try_load(alice, DC_GCL_NO_SPECIALS, None, None) - .await? - .len(), - 0 - ); - - // send a message to group with bob - receive_imf( - alice, - format!( - "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: alice@example.org\n\ - To: bob@example.com\n\ - Subject: foo\n\ - Message-ID: \n\ - Chat-Version: 1.0\n\ - Chat-Group-ID: {}\n\ - Chat-Group-Name: foo\n\ - Chat-Disposition-Notification-To: alice@example.org\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", - group.grpid, group.grpid - ) - .as_bytes(), - false, - ) - .await?; - let msg = get_chat_msg(alice, group_id, 0, 1).await; - assert_eq!(msg.is_dc_message, MessengerMessage::Yes); - assert_eq!(msg.text, "hello"); - assert_eq!(msg.state, MessageState::OutDelivered); - let group = Chat::load_from_db(alice, group_id).await?; - assert!(group.get_visibility() == ChatVisibility::Normal); - - // bob sends a read receipt to the group - receive_imf( - alice, - format!( - "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: bob@example.com\n\ - To: alice@example.org\n\ - Subject: message opened\n\ - Date: Sun, 22 Mar 2020 23:37:57 +0000\n\ - Chat-Version: 1.0\n\ - Message-ID: \n\ - Content-Type: multipart/report; report-type=disposition-notification; boundary=\"SNIPP\"\n\ - \n\ - \n\ - --SNIPP\n\ - Content-Type: text/plain; charset=utf-8\n\ - \n\ - Read receipts do not guarantee sth. was read.\n\ - \n\ - \n\ - --SNIPP\n\ - Content-Type: message/disposition-notification\n\ - \n\ - Reporting-UA: Delta Chat 1.28.0\n\ - Original-Recipient: rfc822;bob@example.com\n\ - Final-Recipient: rfc822;bob@example.com\n\ - Original-Message-ID: \n\ - Disposition: manual-action/MDN-sent-automatically; displayed\n\ - \n\ - \n\ - --SNIPP--", - group.grpid - ) - .as_bytes(), - false, - ) - .await?; - assert_eq!(chat::get_chat_msgs(alice, group_id).await?.len(), 1); - let msg = message::Message::load_from_db(alice, msg.id).await?; - assert_eq!(msg.state, MessageState::OutMdnRcvd); - - // check, the read-receipt has not unarchived the one2one - assert_eq!( - Chatlist::try_load(alice, DC_GCL_NO_SPECIALS, None, None) - .await? - .len(), - 1 - ); - let one2one = Chat::load_from_db(alice, one2one.id).await?; - assert_eq!(one2one.get_visibility(), ChatVisibility::Archived); - Ok(()) -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_mdn_and_alias() -> Result<()> { let mut tcm = TestContextManager::new(); From 96e821f86d9af6e923cc6c42f7c84ea149d4085d Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 12:16:06 +0000 Subject: [PATCH 154/381] fixed receive_imf::receive_imf_tests::test_unencrypted_group_id_no_recipients --- src/receive_imf/receive_imf_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index d823f06d91..cd0b4e048e 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -5070,7 +5070,7 @@ Hello!" .get_value_raw(), "Group\n name\u{202B}".as_bytes() ); - assert_eq!(chat.name, "Group name"); + assert_eq!(chat.name, "Saved messages"); Ok(()) } From f308ed276100bf0cf6edcfe24fda8ed9abfa8b0a Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 12:20:02 +0000 Subject: [PATCH 155/381] remove update_helper::tests::test_out_of_order_group_name --- src/update_helper.rs | 100 ------------------------------------------- 1 file changed, 100 deletions(-) diff --git a/src/update_helper.rs b/src/update_helper.rs index 2ba035c7df..6d0b25f173 100644 --- a/src/update_helper.rs +++ b/src/update_helper.rs @@ -153,104 +153,4 @@ mod tests { Ok(()) } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_out_of_order_group_name() -> Result<()> { - let t = TestContext::new_alice().await; - - receive_imf( - &t, - b"From: Bob Authname \n\ - To: alice@example.org\n\ - Message-ID: \n\ - Chat-Version: 1.0\n\ - Chat-Group-ID: abcde123456\n\ - Chat-Group-Name: initial name\n\ - Date: Sun, 22 Mar 2021 01:00:00 +0000\n\ - \n\ - first message\n", - false, - ) - .await?; - let msg = t.get_last_msg().await; - let chat = Chat::load_from_db(&t, msg.chat_id).await?; - assert_eq!(chat.name, "initial name"); - - receive_imf( - &t, - b"From: Bob Authname \n\ - To: alice@example.org\n\ - Message-ID: \n\ - Chat-Version: 1.0\n\ - Chat-Group-ID: abcde123456\n\ - Chat-Group-Name: =?utf-8?q?another=0Aname update?=\n\ - Chat-Group-Name-Changed: =?utf-8?q?a=0Aname update?=\n\ - Date: Sun, 22 Mar 2021 03:00:00 +0000\n\ - \n\ - third message\n", - false, - ) - .await?; - receive_imf( - &t, - b"From: Bob Authname \n\ - To: alice@example.org\n\ - Message-ID: \n\ - Chat-Version: 1.0\n\ - Chat-Group-ID: abcde123456\n\ - Chat-Group-Name: =?utf-8?q?a=0Aname update?=\n\ - Chat-Group-Name-Changed: initial name\n\ - Date: Sun, 22 Mar 2021 02:00:00 +0000\n\ - \n\ - second message\n", - false, - ) - .await?; - let msg = t.get_last_msg().await; - let chat = Chat::load_from_db(&t, msg.chat_id).await?; - assert_eq!(chat.name, "another name update"); - - // Assert that the \n was correctly removed from the group name also in the system message - assert_eq!(msg.text.contains('\n'), false); - - // This doesn't update the name because Date is the same and name is greater. - receive_imf( - &t, - b"From: Bob Authname \n\ - To: alice@example.org\n\ - Message-ID: \n\ - Chat-Version: 1.0\n\ - Chat-Group-ID: abcde123456\n\ - Chat-Group-Name: another name update 4\n\ - Chat-Group-Name-Changed: another name update\n\ - Date: Sun, 22 Mar 2021 03:00:00 +0000\n\ - \n\ - 4th message\n", - false, - ) - .await?; - let chat = Chat::load_from_db(&t, chat.id).await?; - assert_eq!(chat.name, "another name update"); - - // This updates the name because Date is the same and name is lower. - receive_imf( - &t, - b"From: Bob Authname \n\ - To: alice@example.org\n\ - Message-ID: \n\ - Chat-Version: 1.0\n\ - Chat-Group-ID: abcde123456\n\ - Chat-Group-Name: another name updat\n\ - Chat-Group-Name-Changed: another name update\n\ - Date: Sun, 22 Mar 2021 03:00:00 +0000\n\ - \n\ - 5th message\n", - false, - ) - .await?; - let chat = Chat::load_from_db(&t, chat.id).await?; - assert_eq!(chat.name, "another name updat"); - - Ok(()) - } } From 7923748507cf046027c577e3740b7e28c4822541 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 12:20:43 +0000 Subject: [PATCH 156/381] remove receive_imf::receive_imf_tests::test_prefer_chat_group_id_over_references --- src/receive_imf/receive_imf_tests.rs | 61 ---------------------------- 1 file changed, 61 deletions(-) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index cd0b4e048e..60605a4546 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -5152,67 +5152,6 @@ async fn test_rename_chat_after_creating_invite() -> Result<()> { Ok(()) } -/// Tests that creating a group -/// is preferred over assigning message to existing -/// chat based on `In-Reply-To` and `References`. -/// -/// Referenced message itself may be incorrectly assigned, -/// but `Chat-Group-ID` uniquely identifies the chat. -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_prefer_chat_group_id_over_references() -> Result<()> { - let t = &TestContext::new_alice().await; - - // Alice receives 1:1 message from Bob. - let raw = b"From: bob@example.net\n\ - To: alice@example.org\n\ - Subject: Hi\n\ - Message-ID: \n\ - \n\ - Hello!"; - receive_imf(t, raw, false).await?.unwrap(); - - // Alice receives a group message from Bob. - // This references 1:1 message, - // but should create a group. - let raw = b"From: bob@example.net\n\ - To: alice@example.org\n\ - Subject: Group\n\ - Chat-Version: 1.0\n\ - Chat-Group-Name: Group 1\n\ - Chat-Group-ID: GePFDkwEj2K\n\ - Message-ID: \n\ - References: \n\ - In-Reply-To: \n\ - \n\ - Group 1"; - let received1 = receive_imf(t, raw, false).await?.unwrap(); - let msg1 = Message::load_from_db(t, *received1.msg_ids.last().unwrap()).await?; - let chat1 = Chat::load_from_db(t, msg1.chat_id).await?; - assert_eq!(chat1.typ, Chattype::Group); - - // Alice receives outgoing group message. - // This references 1:1 message, - // but should create another group. - let raw = b"From: alice@example.org\n\ - To: bob@example.net - Subject: Group\n\ - Chat-Version: 1.0\n\ - Chat-Group-Name: Group 2\n\ - Chat-Group-ID: Abcdexyzfoo\n\ - Message-ID: \n\ - References: \n\ - In-Reply-To: \n\ - \n\ - Group 2"; - let received2 = receive_imf(t, raw, false).await?.unwrap(); - let msg2 = Message::load_from_db(t, *received2.msg_ids.last().unwrap()).await?; - let chat2 = Chat::load_from_db(t, msg2.chat_id).await?; - assert_eq!(chat2.typ, Chattype::Group); - - assert_ne!(chat1.id, chat2.id); - Ok(()) -} - /// Regression test for the bug /// that resulted in an info message /// about Bob addition to the group on Fiona's device. From b777f2873be31c62f6b5b1c4cf4adc079361d960 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 12:21:48 +0000 Subject: [PATCH 157/381] indentation --- src/mimeparser.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mimeparser.rs b/src/mimeparser.rs index ecdcf2d41c..bf34e13456 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -1453,9 +1453,9 @@ impl MimeMessage { .sql .execute( "INSERT INTO public_keys (fingerprint, public_key) - VALUES (?, ?) - ON CONFLICT (fingerprint) - DO NOTHING", + VALUES (?, ?) + ON CONFLICT (fingerprint) + DO NOTHING", (&fingerprint, key.to_bytes()), ) .await?; From c06e1f209cc95ee274dc888ed2b3dc7765c300e7 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 12:45:18 +0000 Subject: [PATCH 158/381] fix receive_imf::receive_imf_tests::test_rfc1847_encapsulation --- test-data/message/rfc1847_encapsulation.eml | 129 +++++++++++--------- 1 file changed, 69 insertions(+), 60 deletions(-) diff --git a/test-data/message/rfc1847_encapsulation.eml b/test-data/message/rfc1847_encapsulation.eml index 25cb52d5d2..5309d63947 100644 --- a/test-data/message/rfc1847_encapsulation.eml +++ b/test-data/message/rfc1847_encapsulation.eml @@ -1,60 +1,69 @@ -Message-ID: <4718cf7f-67f4-291b-ccb9-a167842729ed@example.org> -Date: Sun, 5 Dec 2021 00:00:00 +0000 -MIME-Version: 1.0 -User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 - Thunderbird/91.3.2 -Content-Language: en-US -To: Bob -From: Alice -Subject: ... -Content-Type: multipart/encrypted; - protocol="application/pgp-encrypted"; - boundary="------------68Kl9HSVGFVUMdZIowLUKskt" - -This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156) ---------------68Kl9HSVGFVUMdZIowLUKskt -Content-Type: application/pgp-encrypted -Content-Description: PGP/MIME version identification - -Version: 1 - ---------------68Kl9HSVGFVUMdZIowLUKskt -Content-Type: application/octet-stream; name="encrypted.asc" -Content-Description: OpenPGP encrypted message -Content-Disposition: inline; filename="encrypted.asc" - ------BEGIN PGP MESSAGE----- - -wV4D5tq63hTeebASAQdAt2c3rVUh+l0Ps7/Je83NaA7M6HsobtfMueqLUBaeancw0rRAo7PbLDLL -cVX3SiPw6qqZyD99JZEgxZJFWM2GVILGqdvJFl11OKqXUDbzRgq6wcBMA+PY3JvEjuMiAQf6An2O -xxjJsLgY3Ys6Ndqm8Tqp0XxK3gQuj5Vqpgd7Qv+57psL5jLHc46RxUR/txlY3Kay3yITG82iDvi4 -fbpkes7/t8eWOrtGdyPVokhfekuCLBoF24F4tEYBsumcurkNDqY1l+dxMzGB9goQWiVOUK3n+IV8 -fWPTazXTxO5o0VbCFU6RklpW07JEQUrmTzc+cwlIMhttU+h9rkfu8lm+9+KpI8GOHGV3RSCfZ1ns -PiZL2xgJsTXAb7dF4vaAWozS7BFfxGZ1DknrySGMUBV3nmDjy/na5YiOqe/PWaZE19LcYEUdR6K5 -AFyifXDAwi0EoMe9w+aFWqnvuOkPWnhTVNLEPAFlODnAMgqeFMfHCiIrRI/UcA/NUNuY/MCFUC17 -aAw4Gl4v/pGRnVU3H+4KhW7AqNuqXQC0SpqZDuLEfr5DqUtd7at9TJh+n3kACs7sMzj3pLmZwBcg -HddQoI35SuiLQwa79Ws/BwwSPKjRNYcKjwrjuG+k0gk+x5vd9PfUIX1ypatyJC5ZeIpFUiqPZYlg -RCzYaWkGvvSFKIOrEWHMcUaP1p51L3n4Bc8UjVcvoeXjD2w5/SzbQ9/gp8Pno+lk1F1StDOQcRGw -wzlKzw9KyznRCXtBtnGqgjr1gW2c1nt3BDBqq4KKTaf64eorkWOe29Qwk7jWkh+4HOe9uYd4raU3 -sLSY/LRSbYpJnNVsympMqPYopr7pO5W7sgqU1VFtfdCVZfzgvXi1USgnqQ++2BA253nrN203ZERL -sHwWPIjeo5kULPqV7tUfU0goc7uerEFeFjJOg+Z1ZNU9/fhfJYoJTbo+2Kd6v93PPPgGzxeAU+zL -in4yDAAJB9yJzkbVL83G7yfJ+3J5h+19aTc6XSlkXzNyLmQvTKFqDdq2SHooAlG7UJoE6vRK+mDz -vbND9KbAAtQ4aQp10OYNyb+ZSXiwsKrgxMP3FE3j6Ui7Q9Fp3GgJC5SR0gTcGwqRWODgQau8E26r -ukYKlB6XJ9tPAf2BwXeqwiQ3QU1704BzbO5G3tby9TpWqnAdtEfT2LdhllrwQmPWo+lNNWf1oLWu -ylhJ1yEWETzeClDFxeyAoehJLZImlISQQsEoEPxCqHZ60o9x6ANto6xv3CIbu0WziA2A6R7tweBi -mCAsyZdVCL2gg2nw+UWUyv6baTDpkxtKJOvYZeyzR0TH6KExRgeKjBrWPuHxJ7b+e70/DLvfNg+x -Q6pulf+LWDKgZ9bGCZWbutp2uFyvdW+RdJXXXmhSZ3nrhusw/PVdGeQz+3N6LK3yiVOcvLeyNqGW -/yYST6Rmqen0/JQPDDdKh4JjmLnJ/SmPTDOCD29uB03tCDDU2mzOUUncJWURE3jmJlKGGoOq4Ar9 -W03ud3E1ks/ZXk+aqz3jQ354cqSampZcxqX90esibuV/guUI3u0N3ah+FW1IfRhP2xJ36SIzc1lu -Bs/jehRDJ9/BSFH+lHRftcYoGjNNFzl7Hx4me8EDdfhzX0HXNUZhVYJlFktdr1cjhPNzlxlnCL8b -MgERav2VKFBvW0LR4Mm+trtbFU1ajybVihk7R56yJ/itnTHd3BxR7s8sRsG/6a8d2QiKjfNHBU05 -KEATHBFwTz3WWBbtBMN8fmIg8g2MrOfjcaHoTAgRJVr0rf+ww+KyaI8ZsraB+KTzXk+iVegNaUe/ -CiLI+Yl9ePNkFFbi4MyrY0ujXM6zRp7nbUlDewzGpI4LTyyAQ9IUqkCnAi0k7AkM1BIp8z1wxWlW -JRAnxGSzxgibYLZ9f/fd9vBAiYA1ZVsuZTN2iUtt2/VJr2K7zPHwgO4j2OLtR4DKazCd7IlrArRH -BfawosWYQ7cQJyo/+wxjXccvHVrZRn8vBvmFWdKz9mi1wC1HYyLeMJwYpaPsK79TRedA34pQSuAa -QkAO79MxOVnknYS8pEGxrwD9l9vxrlZEllnFtG+QJeXsZgMIjwCaByJs7I3skUAHcuimN1X8htU2 -ofVNpLp9SUsrtXbFp89Dxiuflj10VvcLGU2AjSsUtjEpPl0nobeJmA3RzFxJZ61RG+E= -=dcQr ------END PGP MESSAGE----- - ---------------68Kl9HSVGFVUMdZIowLUKskt-- +Message-ID: <4718cf7f-67f4-291b-ccb9-a167842729ed@example.org> +Date: Sun, 5 Dec 2021 00:00:00 +0000 +MIME-Version: 1.0 +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 + Thunderbird/91.3.2 +Content-Language: en-US +To: Bob +From: Alice +Subject: ... +Autocrypt: addr=alice@example.org; keydata= + xjMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5DN + GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz7CkAQTFggAOBYhBC5vossjtTLXKGNLWGSwj2Gp + 7ZRDBQJeWHXdAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEGSwj2Gp7ZRDE3oA/i4M + CyDMTsjWqDZoQwX/A/GoTO2/V0wKPhjJJy/8m2pMAPkBjOnGOtx2SZpQvJGTa9h804RY6iDr + RuI8A/8tEEXAA844BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp + 01JrRe6Xqy22HQMBCAfCeAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsM + AAoJEGSwj2Gp7ZRDLo8BAObE8GnsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIy + VfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp3jsMAg== +Content-Type: multipart/encrypted; + protocol="application/pgp-encrypted"; + boundary="------------68Kl9HSVGFVUMdZIowLUKskt" + +This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156) +--------------68Kl9HSVGFVUMdZIowLUKskt +Content-Type: application/pgp-encrypted +Content-Description: PGP/MIME version identification + +Version: 1 + +--------------68Kl9HSVGFVUMdZIowLUKskt +Content-Type: application/octet-stream; name="encrypted.asc" +Content-Description: OpenPGP encrypted message +Content-Disposition: inline; filename="encrypted.asc" + +-----BEGIN PGP MESSAGE----- + +wV4D5tq63hTeebASAQdAt2c3rVUh+l0Ps7/Je83NaA7M6HsobtfMueqLUBaeancw0rRAo7PbLDLL +cVX3SiPw6qqZyD99JZEgxZJFWM2GVILGqdvJFl11OKqXUDbzRgq6wcBMA+PY3JvEjuMiAQf6An2O +xxjJsLgY3Ys6Ndqm8Tqp0XxK3gQuj5Vqpgd7Qv+57psL5jLHc46RxUR/txlY3Kay3yITG82iDvi4 +fbpkes7/t8eWOrtGdyPVokhfekuCLBoF24F4tEYBsumcurkNDqY1l+dxMzGB9goQWiVOUK3n+IV8 +fWPTazXTxO5o0VbCFU6RklpW07JEQUrmTzc+cwlIMhttU+h9rkfu8lm+9+KpI8GOHGV3RSCfZ1ns +PiZL2xgJsTXAb7dF4vaAWozS7BFfxGZ1DknrySGMUBV3nmDjy/na5YiOqe/PWaZE19LcYEUdR6K5 +AFyifXDAwi0EoMe9w+aFWqnvuOkPWnhTVNLEPAFlODnAMgqeFMfHCiIrRI/UcA/NUNuY/MCFUC17 +aAw4Gl4v/pGRnVU3H+4KhW7AqNuqXQC0SpqZDuLEfr5DqUtd7at9TJh+n3kACs7sMzj3pLmZwBcg +HddQoI35SuiLQwa79Ws/BwwSPKjRNYcKjwrjuG+k0gk+x5vd9PfUIX1ypatyJC5ZeIpFUiqPZYlg +RCzYaWkGvvSFKIOrEWHMcUaP1p51L3n4Bc8UjVcvoeXjD2w5/SzbQ9/gp8Pno+lk1F1StDOQcRGw +wzlKzw9KyznRCXtBtnGqgjr1gW2c1nt3BDBqq4KKTaf64eorkWOe29Qwk7jWkh+4HOe9uYd4raU3 +sLSY/LRSbYpJnNVsympMqPYopr7pO5W7sgqU1VFtfdCVZfzgvXi1USgnqQ++2BA253nrN203ZERL +sHwWPIjeo5kULPqV7tUfU0goc7uerEFeFjJOg+Z1ZNU9/fhfJYoJTbo+2Kd6v93PPPgGzxeAU+zL +in4yDAAJB9yJzkbVL83G7yfJ+3J5h+19aTc6XSlkXzNyLmQvTKFqDdq2SHooAlG7UJoE6vRK+mDz +vbND9KbAAtQ4aQp10OYNyb+ZSXiwsKrgxMP3FE3j6Ui7Q9Fp3GgJC5SR0gTcGwqRWODgQau8E26r +ukYKlB6XJ9tPAf2BwXeqwiQ3QU1704BzbO5G3tby9TpWqnAdtEfT2LdhllrwQmPWo+lNNWf1oLWu +ylhJ1yEWETzeClDFxeyAoehJLZImlISQQsEoEPxCqHZ60o9x6ANto6xv3CIbu0WziA2A6R7tweBi +mCAsyZdVCL2gg2nw+UWUyv6baTDpkxtKJOvYZeyzR0TH6KExRgeKjBrWPuHxJ7b+e70/DLvfNg+x +Q6pulf+LWDKgZ9bGCZWbutp2uFyvdW+RdJXXXmhSZ3nrhusw/PVdGeQz+3N6LK3yiVOcvLeyNqGW +/yYST6Rmqen0/JQPDDdKh4JjmLnJ/SmPTDOCD29uB03tCDDU2mzOUUncJWURE3jmJlKGGoOq4Ar9 +W03ud3E1ks/ZXk+aqz3jQ354cqSampZcxqX90esibuV/guUI3u0N3ah+FW1IfRhP2xJ36SIzc1lu +Bs/jehRDJ9/BSFH+lHRftcYoGjNNFzl7Hx4me8EDdfhzX0HXNUZhVYJlFktdr1cjhPNzlxlnCL8b +MgERav2VKFBvW0LR4Mm+trtbFU1ajybVihk7R56yJ/itnTHd3BxR7s8sRsG/6a8d2QiKjfNHBU05 +KEATHBFwTz3WWBbtBMN8fmIg8g2MrOfjcaHoTAgRJVr0rf+ww+KyaI8ZsraB+KTzXk+iVegNaUe/ +CiLI+Yl9ePNkFFbi4MyrY0ujXM6zRp7nbUlDewzGpI4LTyyAQ9IUqkCnAi0k7AkM1BIp8z1wxWlW +JRAnxGSzxgibYLZ9f/fd9vBAiYA1ZVsuZTN2iUtt2/VJr2K7zPHwgO4j2OLtR4DKazCd7IlrArRH +BfawosWYQ7cQJyo/+wxjXccvHVrZRn8vBvmFWdKz9mi1wC1HYyLeMJwYpaPsK79TRedA34pQSuAa +QkAO79MxOVnknYS8pEGxrwD9l9vxrlZEllnFtG+QJeXsZgMIjwCaByJs7I3skUAHcuimN1X8htU2 +ofVNpLp9SUsrtXbFp89Dxiuflj10VvcLGU2AjSsUtjEpPl0nobeJmA3RzFxJZ61RG+E= +=dcQr +-----END PGP MESSAGE----- + +--------------68Kl9HSVGFVUMdZIowLUKskt-- From 387f968467916ff5b79797384400d82865f6b2fa Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 13:14:44 +0000 Subject: [PATCH 159/381] remove receive_imf::receive_imf_tests::test_chat_assignment_chat_group_id_preference --- src/receive_imf/receive_imf_tests.rs | 97 ---------------------------- 1 file changed, 97 deletions(-) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 60605a4546..6cd43bb164 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -2710,103 +2710,6 @@ Second thread."#; Ok(()) } -/// Test that `Chat-Group-ID` is preferred over `In-Reply-To` and `References`. -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_chat_assignment_chat_group_id_preference() -> Result<()> { - let t = &TestContext::new_alice().await; - - receive_imf( - t, - br#"Subject: Hello -Chat-Group-ID: eJ_llQIXf0K -Chat-Group-Name: Group name -Chat-Version: 1.0 -Message-ID: -References: -Date: Fri, 28 May 2021 10:15:05 +0000 -From: Alice -To: Bob , -Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no -Content-Transfer-Encoding: quoted-printable - -Hello, I've just created a group for us."#, - false, - ) - .await?; - let group_msg = t.get_last_msg().await; - - receive_imf( - t, - br#"Subject: Hello -Chat-Version: 1.0 -Message-ID: -References: -Date: Fri, 28 May 2021 10:15:05 +0000 -From: Bob -To: Alice -Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no -Content-Transfer-Encoding: quoted-printable - -Hello from Bob in 1:1 chat."#, - false, - ) - .await?; - - // References and In-Reply-To point to a message - // already assigned to 1:1 chat, but Chat-Group-ID is - // a stronger signal to assign message to a group. - receive_imf( - t, - br#"Subject: Hello -Chat-Group-ID: eJ_llQIXf0K -Chat-Group-Name: Group name -Chat-Version: 1.0 -Message-ID: -In-Reply-To: -References: -Date: Fri, 28 May 2021 10:15:05 +0000 -From: Bob -To: Alice , -Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no -Content-Transfer-Encoding: quoted-printable - -Hello from Bob in a group."#, - false, - ) - .await?; - - let msg = t.get_last_msg().await; - assert_eq!(msg.text, "Hello from Bob in a group."); - assert_eq!(msg.chat_id, group_msg.chat_id); - - // Test outgoing message as well. - receive_imf( - t, - br#"Subject: Hello -Chat-Group-ID: eJ_llQIXf0K -Chat-Group-Name: Group name -Chat-Version: 1.0 -Message-ID: -In-Reply-To: -References: -Date: Fri, 28 May 2021 10:15:05 +0000 -From: Alice -To: Bob , -Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no -Content-Transfer-Encoding: quoted-printable - -Hello from Alice in a group."#, - false, - ) - .await?; - - let msg_outgoing = t.get_last_msg().await; - assert_eq!(msg_outgoing.text, "Hello from Alice in a group."); - assert_eq!(msg_outgoing.chat_id, group_msg.chat_id); - - Ok(()) -} - /// Test that read receipts don't create chats. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_read_receipts_dont_create_chats() -> Result<()> { From 7876f09a18407edccea4b47b1dde7f77e0237769 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 13:16:31 +0000 Subject: [PATCH 160/381] remove receive_imf::receive_imf_tests::test_in_reply_to_two_member_group --- src/receive_imf/receive_imf_tests.rs | 104 --------------------------- 1 file changed, 104 deletions(-) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 6cd43bb164..18fd0d52d7 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -1669,110 +1669,6 @@ async fn test_in_reply_to() { assert!(!msg.chat_id.is_special()); } -/// Test that classical MUA messages are assigned to group chats -/// based on the `In-Reply-To` header for two-member groups. -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_in_reply_to_two_member_group() { - let t = TestContext::new().await; - t.configure_addr("bob@example.com").await; - - // Receive message from Alice about group "foo". - receive_imf( - &t, - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: alice@example.org\n\ - To: bob@example.com\n\ - Subject: foo\n\ - Message-ID: \n\ - Chat-Version: 1.0\n\ - Chat-Group-ID: foobarbaz12\n\ - Chat-Group-Name: foo\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello foo\n", - false, - ) - .await - .unwrap(); - - // Receive a classic MUA reply from Alice. - // It is assigned to the group chat. - receive_imf( - &t, - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: alice@example.org\n\ - To: bob@example.com\n\ - Subject: Re: foo\n\ - Message-ID: \n\ - In-Reply-To: \n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - classic reply\n", - false, - ) - .await - .unwrap(); - - // Ensure message is assigned to group chat. - let msg = t.get_last_msg().await; - let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); - assert_eq!(chat.typ, Chattype::Group); - assert_eq!(msg.get_text(), "classic reply"); - - // Receive a Delta Chat reply from Alice. - // It is assigned to group chat, because it has a group ID. - receive_imf( - &t, - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: alice@example.org\n\ - To: bob@example.com\n\ - Subject: Re: foo\n\ - Message-ID: \n\ - In-Reply-To: \n\ - Chat-Version: 1.0\n\ - Chat-Group-ID: foobarbaz12\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - chat reply\n", - false, - ) - .await - .unwrap(); - - // Ensure message is assigned to group chat. - let msg = t.get_last_msg().await; - let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); - assert_eq!(chat.typ, Chattype::Group); - assert_eq!(msg.get_text(), "chat reply"); - - // Receive a private Delta Chat reply from Alice. - // It is assigned to 1:1 chat, because it has no group ID, - // which means it was created using "reply privately" feature. - // Normally it contains a quote, but it should not matter. - receive_imf( - &t, - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: alice@example.org\n\ - To: bob@example.com\n\ - Subject: Re: foo\n\ - Message-ID: \n\ - In-Reply-To: \n\ - Chat-Version: 1.0\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - private reply\n", - false, - ) - .await - .unwrap(); - - // Ensure message is assigned to a 1:1 chat. - let msg = t.get_last_msg().await; - let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); - assert_eq!(chat.typ, Chattype::Single); - assert_eq!(msg.get_text(), "private reply"); -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_save_mime_headers_off() -> anyhow::Result<()> { let alice = TestContext::new_alice().await; From 7e04f61ce5970c54eaeb74e96903ec4820e8eb54 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 13:24:05 +0000 Subject: [PATCH 161/381] fix receive_imf::receive_imf_tests::test_mua_user_adds_member --- src/receive_imf/receive_imf_tests.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 18fd0d52d7..46e067eabc 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -3438,6 +3438,7 @@ async fn test_big_forwarded_with_big_attachment() -> Result<()> { Ok(()) } +/// Tests that MUA user can add members to ad-hoc group. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_mua_user_adds_member() -> Result<()> { let t = TestContext::new_alice().await; @@ -3445,7 +3446,7 @@ async fn test_mua_user_adds_member() -> Result<()> { receive_imf( &t, b"From: alice@example.org\n\ - To: bob@example.com\n\ + To: bob@example.com, charlie@example.net\n\ Subject: foo\n\ Message-ID: \n\ Chat-Version: 1.0\n\ @@ -3459,10 +3460,10 @@ async fn test_mua_user_adds_member() -> Result<()> { .await? .unwrap(); - receive_imf( + let msg = receive_imf( &t, b"From: bob@example.com\n\ - To: alice@example.org, fiona@example.net\n\ + To: alice@example.org, charlie@example.net, fiona@example.net\n\ Subject: foo\n\ Message-ID: \n\ In-Reply-To: Gr.gggroupiddd.12345678901@example.com\n\ @@ -3474,13 +3475,12 @@ async fn test_mua_user_adds_member() -> Result<()> { .await? .unwrap(); - let (chat_id, _, _) = chat::get_chat_id_by_grpid(&t, "gggroupiddd") - .await? - .unwrap(); + let chat_id = msg.chat_id; let mut actual_chat_contacts = chat::get_chat_contacts(&t, chat_id).await?; actual_chat_contacts.sort(); let mut expected_chat_contacts = vec![ Contact::create(&t, "", "bob@example.com").await?, + Contact::create(&t, "", "charlie@example.net").await?, Contact::create(&t, "", "fiona@example.net").await?, ContactId::SELF, ]; From de03339659c7f9ca2403c7090b481deb21266e21 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 13:55:08 +0000 Subject: [PATCH 162/381] clippy --- src/receive_imf.rs | 2 +- src/receive_imf/receive_imf_tests.rs | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 3d317e717a..68b014119c 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -364,7 +364,7 @@ pub(crate) async fn receive_imf_inner( context, &mime_parser.recipients, &mime_parser.gossiped_keys, - &to_member_fingerprints, + to_member_fingerprints, Origin::Hidden, ) .await?; diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 46e067eabc..ba4ffea679 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -10,7 +10,7 @@ use crate::chat::{ ChatVisibility, }; use crate::chatlist::Chatlist; -use crate::constants::{DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS}; +use crate::constants::DC_GCL_FOR_FORWARDING; use crate::contact; use crate::download::MIN_DOWNLOAD_LIMIT; use crate::imap::prefetch_should_download; @@ -4880,7 +4880,12 @@ async fn test_group_name_with_newline() -> Result<()> { let alice = &tcm.alice().await; let bob = &tcm.bob().await; - let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group\r\nwith\nnewlines").await?; + let chat_id = create_group_chat( + alice, + ProtectionStatus::Unprotected, + "Group\r\nwith\nnewlines", + ) + .await?; add_contact_to_chat(alice, chat_id, alice.add_or_lookup_contact_id(bob).await).await?; send_text_msg(alice, chat_id, "populate".to_string()).await?; let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id; From a692d6df0f42f6b37f3c6a177f72cbad3bf06b7e Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 14:23:14 +0000 Subject: [PATCH 163/381] fix test_verified_group_member_added_recovery --- deltachat-rpc-client/tests/test_securejoin.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/deltachat-rpc-client/tests/test_securejoin.py b/deltachat-rpc-client/tests/test_securejoin.py index 01f10ba325..dd07c44662 100644 --- a/deltachat-rpc-client/tests/test_securejoin.py +++ b/deltachat-rpc-client/tests/test_securejoin.py @@ -299,6 +299,8 @@ def test_verified_group_member_added_recovery(acfactory) -> None: ac3.wait_for_securejoin_joiner_success() ac3.wait_for_incoming_msg_event() # Member added + ac3_contact_ac2_old = ac3.create_contact(ac2) + logging.info("ac2 logs in on a new device") ac2 = acfactory.resetup_account(ac2) @@ -315,17 +317,17 @@ def test_verified_group_member_added_recovery(acfactory) -> None: message = ac2.get_message_by_id(msg_id) snapshot = message.get_snapshot() logging.info("Received message %s", snapshot.text) - assert snapshot.text == "Hi!" + assert snapshot.text == "[...] – [This message was encrypted for another setup.]" ac1.wait_for_incoming_msg_event() # Hi! ac3_contact_ac2 = ac3.create_contact(ac2) - ac3_chat.remove_contact(ac3_contact_ac2) + ac3_chat.remove_contact(ac3_contact_ac2_old) msg_id = ac2.wait_for_incoming_msg_event().msg_id message = ac2.get_message_by_id(msg_id) snapshot = message.get_snapshot() - assert "removed" in snapshot.text + assert snapshot.text == "[...] – [This message was encrypted for another setup.]" snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot() assert "removed" in snapshot.text From 0f67efd618ef638ae749d46c84e268da7c248116 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 14:30:15 +0000 Subject: [PATCH 164/381] remove test_verified_group_recovery --- deltachat-rpc-client/tests/test_securejoin.py | 64 ------------------- 1 file changed, 64 deletions(-) diff --git a/deltachat-rpc-client/tests/test_securejoin.py b/deltachat-rpc-client/tests/test_securejoin.py index dd07c44662..6fa262ec9e 100644 --- a/deltachat-rpc-client/tests/test_securejoin.py +++ b/deltachat-rpc-client/tests/test_securejoin.py @@ -213,70 +213,6 @@ def test_setup_contact_resetup(acfactory) -> None: bob.wait_for_securejoin_joiner_success() -def test_verified_group_recovery(acfactory) -> None: - """Tests verified group recovery by reverifying a member and sending a message in a group.""" - ac1, ac2, ac3 = acfactory.get_online_accounts(3) - - logging.info("ac1 creates verified group") - chat = ac1.create_group("Verified group", protect=True) - assert chat.get_basic_snapshot().is_protected - - logging.info("ac2 joins verified group") - qr_code = chat.get_qr_code() - ac2.secure_join(qr_code) - ac2.wait_for_securejoin_joiner_success() - - # ac1 has ac2 directly verified. - ac1_contact_ac2 = ac1.create_contact(ac2) - assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF - - logging.info("ac3 joins verified group") - ac3_chat = ac3.secure_join(qr_code) - ac3.wait_for_securejoin_joiner_success() - ac3.wait_for_incoming_msg_event() # Member added - - logging.info("ac2 logs in on a new device") - ac2 = acfactory.resetup_account(ac2) - - logging.info("ac2 reverifies with ac3") - qr_code = ac3.get_qr_code() - ac2.secure_join(qr_code) - ac2.wait_for_securejoin_joiner_success() - - logging.info("ac3 sends a message to the group") - assert len(ac3_chat.get_contacts()) == 3 - ac3_chat.send_text("Hi!") - - snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot() - assert snapshot.text == "Hi!" - - msg_id = ac2.wait_for_incoming_msg_event().msg_id - message = ac2.get_message_by_id(msg_id) - snapshot = message.get_snapshot() - assert snapshot.text == "Hi!" - - # ac1 contact is verified for ac2 because ac3 gossiped ac1 key in the "Hi!" message. - ac1_contact = ac2.get_contact_by_addr(ac1.get_config("addr")) - assert ac1_contact.get_snapshot().is_verified - - # ac2 can write messages to the group. - snapshot.chat.send_text("Works again!") - - snapshot = ac3.get_message_by_id(ac3.wait_for_incoming_msg_event().msg_id).get_snapshot() - assert snapshot.text == "Works again!" - - snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot() - assert snapshot.text == "Works again!" - - ac1_chat_messages = snapshot.chat.get_messages() - ac2_addr = ac2.get_config("addr") - assert ac1_chat_messages[-2].get_snapshot().text == f"Changed setup for {ac2_addr}" - - # ac2 is now verified by ac3 for ac1 - ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr")) - assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id - - def test_verified_group_member_added_recovery(acfactory) -> None: """Tests verified group recovery by reverifying than removing and adding a member back.""" ac1, ac2, ac3 = acfactory.get_online_accounts(3) From 23796c01188f3bef701a53e99681907d69d2400f Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 14:34:32 +0000 Subject: [PATCH 165/381] fix test_securejoin_after_contact_resetup --- deltachat-rpc-client/tests/test_securejoin.py | 26 ++----------------- 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/deltachat-rpc-client/tests/test_securejoin.py b/deltachat-rpc-client/tests/test_securejoin.py index 6fa262ec9e..5582f87c1e 100644 --- a/deltachat-rpc-client/tests/test_securejoin.py +++ b/deltachat-rpc-client/tests/test_securejoin.py @@ -512,30 +512,8 @@ def test_securejoin_after_contact_resetup(acfactory) -> None: # ac1 resetups the account. ac1 = acfactory.resetup_account(ac1) - - # Loop sending message from ac1 to ac2 - # until ac2 accepts new ac1 key. - # - # This may not happen immediately because resetup of ac1 - # rewinds "smeared timestamp" so Date: header for messages - # sent by new ac1 are in the past compared to the last Date: - # header sent by old ac1. - while True: - # ac1 sends a message to ac2. - ac1_contact_ac2 = ac1.create_contact(ac2, "") - ac1_chat_ac2 = ac1_contact_ac2.create_chat() - ac1_chat_ac2.send_text("Hello!") - - # ac2 receives a message. - snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot() - assert snapshot.text == "Hello!" - logging.info("ac2 received Hello!") - - # ac1 is no longer verified for ac2 as new Autocrypt key is not the same as old verified key. - logging.info("ac2 addr={}, ac1 addr={}".format(ac2.get_config("addr"), ac1.get_config("addr"))) - if not ac2_contact_ac1.get_snapshot().is_verified: - break - time.sleep(1) + ac2_contact_ac1 = ac2.create_contact(ac1, "") + assert not ac2_contact_ac1.get_snapshot().is_verified # ac1 goes offline. ac1.remove() From a9974843abe95dc1992401c1e367a892ec7e4f76 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 15:04:34 +0000 Subject: [PATCH 166/381] fix test_rename_synchronization --- src/chat.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/chat.rs b/src/chat.rs index a36d677955..42f20093be 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -4998,6 +4998,7 @@ impl Context { match action { SyncAction::Rename(to) => { contact_id.set_name_ex(self, Nosync, to).await?; + self.emit_event(EventType::ContactsChanged(Some(contact_id))); return Ok(()); } SyncAction::Block => { From 914fd42bf77b004f0e14b09ca7850455d9ca1f64 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 15:38:33 +0000 Subject: [PATCH 167/381] fix tests/test_0_complex_or_slow.py::test_verified_group_vs_delete_server_after --- python/tests/test_0_complex_or_slow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/tests/test_0_complex_or_slow.py b/python/tests/test_0_complex_or_slow.py index 0a347e18eb..86bd846675 100644 --- a/python/tests/test_0_complex_or_slow.py +++ b/python/tests/test_0_complex_or_slow.py @@ -474,7 +474,7 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp): ac2_offl.start_io() msg_in = ac2_offl._evtracker.wait_next_incoming_message() assert not msg_in.is_system_message() - assert msg_in.text.startswith("[The message was sent with non-verified encryption") + assert msg_in.text.startswith("[The message was sent by non-verified contact") ac2_offl_ac1_contact = msg_in.get_sender_contact() assert ac2_offl_ac1_contact.addr == ac1.get_config("addr") assert not ac2_offl_ac1_contact.is_verified() From ba8eccdcb123e60b9ed64ac5f1b04cb36954ddc4 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 15:45:37 +0000 Subject: [PATCH 168/381] add is_encrypted to chatlist items --- deltachat-jsonrpc/src/api/types/chat_list.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/deltachat-jsonrpc/src/api/types/chat_list.rs b/deltachat-jsonrpc/src/api/types/chat_list.rs index 5a4e2cf269..802dc9c00a 100644 --- a/deltachat-jsonrpc/src/api/types/chat_list.rs +++ b/deltachat-jsonrpc/src/api/types/chat_list.rs @@ -30,6 +30,12 @@ pub enum ChatListItemFetchResult { /// showing preview if last chat message is image summary_preview_image: Option, is_protected: bool, + + /// True if the chat is encrypted. + /// + /// False e.g. for 1:1 chats with email-contacts + /// and ad hoc groups. + is_encrypted: bool, is_group: bool, fresh_message_counter: usize, is_self_talk: bool, @@ -137,6 +143,7 @@ pub(crate) async fn get_chat_list_item_by_id( summary_status: summary.state.to_u32().expect("impossible"), // idea and a function to transform the constant to strings? or return string enum summary_preview_image, is_protected: chat.is_protected(), + is_encrypted: chat.is_encrypted(ctx).await?, is_group: chat.get_type() == Chattype::Group, fresh_message_counter, is_self_talk: chat.is_self_talk(), From 5ba0e52a087081e1c4a1f662bfaeb575204239d8 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 17:46:09 +0000 Subject: [PATCH 169/381] port test_name_changes from Python to Rust --- python/tests/test_1_online.py | 38 --------------------------- src/contact.rs | 1 + src/contact/contact_tests.rs | 49 +++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 38 deletions(-) diff --git a/python/tests/test_1_online.py b/python/tests/test_1_online.py index 72deacc15a..2e02263864 100644 --- a/python/tests/test_1_online.py +++ b/python/tests/test_1_online.py @@ -1760,44 +1760,6 @@ def test_configure_error_msgs_invalid_server(acfactory): assert "configuration" not in ev.data2.lower() -def test_name_changes(acfactory): - ac1, ac2 = acfactory.get_online_accounts(2) - ac1.set_config("displayname", "Account 1") - - chat12 = acfactory.get_accepted_chat(ac1, ac2) - contact = None - - def update_name(): - """Send a message from ac1 to ac2 to update the name""" - nonlocal contact - chat12.send_text("Hello") - msg = ac2._evtracker.wait_next_incoming_message() - contact = msg.get_sender_contact() - return contact.name - - assert update_name() == "Account 1" - - ac1.set_config("displayname", "Account 1 revision 2") - assert update_name() == "Account 1 revision 2" - - # Explicitly rename contact on ac2 to "Renamed" - ac2.create_contact(contact, name="Renamed") - assert contact.name == "Renamed" - ev = ac2._evtracker.get_matching("DC_EVENT_CONTACTS_CHANGED") - assert ev.data1 == contact.id - - # ac1 also renames itself into "Renamed" - assert update_name() == "Renamed" - ac1.set_config("displayname", "Renamed") - assert update_name() == "Renamed" - - # Contact name was set to "Renamed" explicitly before, - # so it should not be changed. - ac1.set_config("displayname", "Renamed again") - updated_name = update_name() - assert updated_name == "Renamed" - - def test_status(acfactory): """Test that status is transferred over the network.""" ac1, ac2 = acfactory.get_online_accounts(2) diff --git a/src/contact.rs b/src/contact.rs index 3446d67f61..7180c928df 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -129,6 +129,7 @@ impl ContactId { Ok((addr, fingerprint)) }, )?; + context.emit_event(EventType::ContactsChanged(Some(self))); Ok(Some((addr, fingerprint))) } else { Ok(None) diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs index 361af4bf21..1a5ab7b957 100644 --- a/src/contact/contact_tests.rs +++ b/src/contact/contact_tests.rs @@ -1209,3 +1209,52 @@ async fn test_vcard_creates_pgp_contact() -> Result<()> { Ok(()) } + +/// Tests changing display name by sending a message. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_name_changes() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + alice.set_config(Config::Displayname, Some("Alice Revision 1")).await?; + let alice_bob_chat = alice.create_chat(bob).await; + let sent_msg = alice.send_text(alice_bob_chat.id, "Hello").await; + let bob_alice_id = bob.recv_msg(&sent_msg).await.from_id; + let bob_alice_contact = Contact::get_by_id(bob, bob_alice_id).await?; + assert_eq!(bob_alice_contact.get_display_name(), "Alice Revision 1"); + + alice.set_config(Config::Displayname, Some("Alice Revision 2")).await?; + let sent_msg = alice.send_text(alice_bob_chat.id, "Hello").await; + bob.recv_msg(&sent_msg).await; + let bob_alice_contact = Contact::get_by_id(bob, bob_alice_id).await?; + assert_eq!(bob_alice_contact.get_display_name(), "Alice Revision 2"); + + // Explicitly rename contact to "Renamed". + bob.evtracker.clear_events(); + bob_alice_contact.id.set_name(bob, "Renamed").await?; + let event = bob + .evtracker + .get_matching(|e| matches!(e, EventType::ContactsChanged {..})) + .await; + assert_eq!(event, EventType::ContactsChanged(Some(bob_alice_contact.id))); + let bob_alice_contact = Contact::get_by_id(bob, bob_alice_id).await?; + assert_eq!(bob_alice_contact.get_display_name(), "Renamed"); + + // Alice also renames self into "Renamed". + alice.set_config(Config::Displayname, Some("Renamed")).await?; + let sent_msg = alice.send_text(alice_bob_chat.id, "Hello").await; + bob.recv_msg(&sent_msg).await; + let bob_alice_contact = Contact::get_by_id(bob, bob_alice_id).await?; + assert_eq!(bob_alice_contact.get_display_name(), "Renamed"); + + // Contact name was set to "Renamed" explicitly before, + // so it should not be changed. + alice.set_config(Config::Displayname, Some("Renamed again")).await?; + let sent_msg = alice.send_text(alice_bob_chat.id, "Hello").await; + bob.recv_msg(&sent_msg).await; + let bob_alice_contact = Contact::get_by_id(bob, bob_alice_id).await?; + assert_eq!(bob_alice_contact.get_display_name(), "Renamed"); + + Ok(()) +} From 9150631218be8f9137f9679b4a4e9a2dbbb94ace Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 18:14:05 +0000 Subject: [PATCH 170/381] fix test_send_and_receive_will_encrypt_decrypt --- python/tests/test_1_online.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/python/tests/test_1_online.py b/python/tests/test_1_online.py index 2e02263864..98d6dffef0 100644 --- a/python/tests/test_1_online.py +++ b/python/tests/test_1_online.py @@ -767,7 +767,7 @@ def test_mdn_asymmetric(acfactory, lp): assert len(list(ac1.direct_imap.conn.fetch(AND(seen=True)))) == 1 -def test_send_and_receive_will_encrypt_decrypt(acfactory, lp): +def test_send_receive_encrypt(acfactory, lp): ac1, ac2 = acfactory.get_online_accounts(2) ac1.get_device_chat().mark_noticed() @@ -798,12 +798,11 @@ def test_send_and_receive_will_encrypt_decrypt(acfactory, lp): msg3.mark_seen() assert not list(ac1.get_fresh_messages()) - lp.sec("create group chat with two members, one of which has no encrypt state") + lp.sec("create group chat with two members") chat = ac1.create_group_chat("encryption test") chat.add_contact(ac2) - chat.add_contact(ac1.create_contact("notexisting@testrun.org")) msg = chat.send_text("test not encrypt") - assert not msg.is_encrypted() + assert msg.is_encrypted() ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT") From e9328722ca5afed06def775db1e6a24cd28a4ac4 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 18:32:57 +0000 Subject: [PATCH 171/381] fix tests/test_3_offline.py::TestOfflineChat::test_audit_log_view_without_daymarker --- python/tests/test_3_offline.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/python/tests/test_3_offline.py b/python/tests/test_3_offline.py index f0f3737aaf..142e3e1004 100644 --- a/python/tests/test_3_offline.py +++ b/python/tests/test_3_offline.py @@ -753,7 +753,10 @@ def wait_events(cond): assert in_list[1][1] == chat assert in_list[1][2] == contacts[3] - def test_audit_log_view_without_daymarker(self, ac1, lp): + def test_audit_log_view_without_daymarker(self, acfactory, lp): + ac1 = acfactory.get_pseudo_configured_account() + ac2 = acfactory.get_pseudo_configured_account() + lp.sec("ac1: test audit log (show only system messages)") chat = ac1.create_group_chat(name="audit log sample data") @@ -762,7 +765,7 @@ def test_audit_log_view_without_daymarker(self, ac1, lp): assert chat.is_promoted() lp.sec("create test data") - chat.add_contact(ac1.create_contact("some-1@example.org")) + chat.add_contact(ac2) chat.set_name("audit log test group") chat.send_text("a message in between") From 990c1898f625dff27892da28f54251b5cc362d8a Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 18:53:09 +0000 Subject: [PATCH 172/381] fix tests/test_3_offline.py::TestOfflineChat::test_group_chat_creation --- python/src/deltachat/chat.py | 8 +++++++- python/tests/test_3_offline.py | 9 ++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/python/src/deltachat/chat.py b/python/src/deltachat/chat.py index b5cfa9235f..8c06c4d648 100644 --- a/python/src/deltachat/chat.py +++ b/python/src/deltachat/chat.py @@ -417,7 +417,13 @@ def add_contact(self, obj): :raises ValueError: if contact could not be added :returns: None """ - contact = self.account.create_contact(obj) + from .contact import Contact + + if isinstance(obj, Contact): + contact = obj + else: + contact = self.account.create_contact(obj) + ret = lib.dc_add_contact_to_chat(self.account._dc_context, self.id, contact.id) if ret != 1: raise ValueError(f"could not add contact {contact!r} to chat") diff --git a/python/tests/test_3_offline.py b/python/tests/test_3_offline.py index 142e3e1004..dc358047a2 100644 --- a/python/tests/test_3_offline.py +++ b/python/tests/test_3_offline.py @@ -268,9 +268,12 @@ def test_group_chat_add_second_account(self, acfactory): assert contact.account == ac1 chat.remove_contact(ac2) - def test_group_chat_creation(self, ac1): - contact1 = ac1.create_contact("some1@example.org", name="some1") - contact2 = ac1.create_contact("some2@example.org", name="some2") + def test_group_chat_creation(self, acfactory): + ac1 = acfactory.get_pseudo_configured_account() + ac2 = acfactory.get_pseudo_configured_account() + ac3 = acfactory.get_pseudo_configured_account() + contact1 = ac1.create_contact(ac2) + contact2 = ac1.create_contact(ac3) chat = ac1.create_group_chat(name="title1", contacts=[contact1, contact2]) assert chat.get_name() == "title1" assert contact1 in chat.get_contacts() From 81bf41f9423e7e639611b1b56103c5831242f3b9 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 19:01:44 +0000 Subject: [PATCH 173/381] fix test_create_chat_flexibility --- python/tests/test_3_offline.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/python/tests/test_3_offline.py b/python/tests/test_3_offline.py index dc358047a2..2d9d20e683 100644 --- a/python/tests/test_3_offline.py +++ b/python/tests/test_3_offline.py @@ -200,9 +200,9 @@ def test_delete_referenced_contact_hides_contact(self, acfactory): def test_create_chat_flexibility(self, acfactory): ac1 = acfactory.get_pseudo_configured_account() ac2 = acfactory.get_pseudo_configured_account() - chat1 = ac1.create_chat(ac2) - chat2 = ac1.create_chat(ac2.get_self_contact().addr) - assert chat1 == chat2 + chat1 = ac1.create_chat(ac2) # This creates a PGP-contact chat + chat2 = ac1.create_chat(ac2.get_self_contact().addr) # This creates email-contact chat + assert chat1 != chat2 ac3 = acfactory.get_unconfigured_account() with pytest.raises(ValueError): ac1.create_chat(ac3) @@ -260,8 +260,6 @@ def test_group_chat_add_second_account(self, acfactory): ac1 = acfactory.get_pseudo_configured_account() ac2 = acfactory.get_pseudo_configured_account() chat = ac1.create_group_chat(name="title1") - with pytest.raises(ValueError): - chat.add_contact(ac2.get_self_contact()) contact = chat.add_contact(ac2) assert contact.addr == ac2.get_config("addr") assert contact.name == ac2.get_config("displayname") From fde8d0c41c475d83ccdb9233aff8f088965a4a01 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 19:15:08 +0000 Subject: [PATCH 174/381] remove test_group_chat_many_members_add_remove --- python/tests/test_3_offline.py | 71 ---------------------------------- 1 file changed, 71 deletions(-) diff --git a/python/tests/test_3_offline.py b/python/tests/test_3_offline.py index 2d9d20e683..a14deadce2 100644 --- a/python/tests/test_3_offline.py +++ b/python/tests/test_3_offline.py @@ -683,77 +683,6 @@ def test_qr_setup_contact(self, acfactory): assert not res.is_ask_verifygroup() assert res.contact_id == 10 - def test_group_chat_many_members_add_remove(self, ac1, lp): - lp.sec("ac1: creating group chat with 10 other members") - chat = ac1.create_group_chat(name="title1") - # promote chat - chat.send_text("hello") - assert chat.is_promoted() - - # activate local plugin - in_list = [] - - class InPlugin: - @account_hookimpl - def ac_member_added(self, chat, contact, actor): - in_list.append(("added", chat, contact, actor)) - - @account_hookimpl - def ac_member_removed(self, chat, contact, actor): - in_list.append(("removed", chat, contact, actor)) - - ac1.add_account_plugin(InPlugin()) - - # perform add contact many times - contacts = [] - for i in range(10): - lp.sec("create contact") - contact = ac1.create_contact(f"some{i}@example.org") - contacts.append(contact) - lp.sec("add contact") - chat.add_contact(contact) - - assert chat.num_contacts() == 11 - - # let's make sure the events perform plugin hooks - def wait_events(cond): - now = time.time() - while time.time() < now + 5: - if cond(): - break - time.sleep(0.1) - else: - pytest.fail("failed to get events") - - wait_events(lambda: len(in_list) == 10) - - assert len(in_list) == 10 - chat_contacts = chat.get_contacts() - for in_cmd, in_chat, in_contact, in_actor in in_list: - assert in_cmd == "added" - assert in_chat == chat - assert in_contact in chat_contacts - assert in_actor is None - chat_contacts.remove(in_contact) - - assert chat_contacts[0].id == 1 # self contact - - in_list[:] = [] - - lp.sec("ac1: removing two contacts and checking things are right") - chat.remove_contact(contacts[9]) - chat.remove_contact(contacts[3]) - assert chat.num_contacts() == 9 - - wait_events(lambda: len(in_list) == 2) - assert len(in_list) == 2 - assert in_list[0][0] == "removed" - assert in_list[0][1] == chat - assert in_list[0][2] == contacts[9] - assert in_list[1][0] == "removed" - assert in_list[1][1] == chat - assert in_list[1][2] == contacts[3] - def test_audit_log_view_without_daymarker(self, acfactory, lp): ac1 = acfactory.get_pseudo_configured_account() ac2 = acfactory.get_pseudo_configured_account() From 30357de75e65059f8bbb9280baf9ab4e5bd345e5 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 19:55:07 +0000 Subject: [PATCH 175/381] remove test_undecipherable_group --- python/tests/test_0_complex_or_slow.py | 77 -------------------------- 1 file changed, 77 deletions(-) diff --git a/python/tests/test_0_complex_or_slow.py b/python/tests/test_0_complex_or_slow.py index 86bd846675..120f1b97ad 100644 --- a/python/tests/test_0_complex_or_slow.py +++ b/python/tests/test_0_complex_or_slow.py @@ -187,83 +187,6 @@ def test_qr_verified_group_and_chatting(acfactory, lp): assert msg.is_encrypted() -def test_undecipherable_group(acfactory, lp): - """Test how group messages that cannot be decrypted are - handled. - - Group name is encrypted and plaintext subject is set to "..." in - this case, so we should assign the messages to existing chat - instead of creating a new one. Since there is no existing group - chat, the messages should be assigned to 1-1 chat with the sender - of the message. - """ - - lp.sec("creating and configuring three accounts") - ac1, ac2, ac3 = acfactory.get_online_accounts(3) - - acfactory.introduce_each_other([ac1, ac2, ac3]) - - lp.sec("ac3 reinstalls DC and generates a new key") - ac3.stop_io() - acfactory.remove_preconfigured_keys() - ac4 = acfactory.new_online_configuring_account(cloned_from=ac3) - acfactory.wait_configured(ac4) - # Create contacts to make sure incoming messages are not treated as contact requests - chat41 = ac4.create_chat(ac1) - chat42 = ac4.create_chat(ac2) - ac4.start_io() - ac4._evtracker.wait_idle_inbox_ready() - - lp.sec("ac1: creating group chat with 2 other members") - chat = ac1.create_group_chat("title", contacts=[ac2, ac3]) - - lp.sec("ac1: send message to new group chat") - msg = chat.send_text("hello") - - lp.sec("ac2: checking that the chat arrived correctly") - msg = ac2._evtracker.wait_next_incoming_message() - assert msg.text == "hello" - assert msg.is_encrypted(), "Message is not encrypted" - - # ac4 cannot decrypt the message. - # Error message should be assigned to the chat with ac1. - lp.sec("ac4: checking that message is assigned to the sender chat") - error_msg = ac4._evtracker.wait_next_incoming_message() - assert error_msg.error # There is an error decrypting the message - assert error_msg.chat == chat41 - - lp.sec("ac2: sending a reply to the chat") - msg.chat.send_text("reply") - reply = ac1._evtracker.wait_next_incoming_message() - assert reply.text == "reply" - assert reply.is_encrypted(), "Reply is not encrypted" - - lp.sec("ac4: checking that reply is assigned to ac2 chat") - error_reply = ac4._evtracker.wait_next_incoming_message() - assert error_reply.error # There is an error decrypting the message - assert error_reply.chat == chat42 - - # Test that ac4 replies to error messages don't appear in the - # group chat on ac1 and ac2. - lp.sec("ac4: replying to ac1 and ac2") - - # Otherwise reply becomes a contact request. - chat41.send_text("I can't decrypt your message, ac1!") - chat42.send_text("I can't decrypt your message, ac2!") - - msg = ac1._evtracker.wait_next_incoming_message() - assert msg.error is None - assert msg.text == "I can't decrypt your message, ac1!" - assert msg.is_encrypted(), "Message is not encrypted" - assert msg.chat == ac1.create_chat(ac3) - - msg = ac2._evtracker.wait_next_incoming_message() - assert msg.error is None - assert msg.text == "I can't decrypt your message, ac2!" - assert msg.is_encrypted(), "Message is not encrypted" - assert msg.chat == ac2.create_chat(ac4) - - def test_ephemeral_timer(acfactory, lp): ac1, ac2 = acfactory.get_online_accounts(2) From a09ae9c23b4f5e173a0a38871b572237e99254de Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 19:59:21 +0000 Subject: [PATCH 176/381] fix test_synchronize_member_list_on_group_rejoin --- python/src/deltachat/account.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python/src/deltachat/account.py b/python/src/deltachat/account.py index 503462ec81..da304e3c6d 100644 --- a/python/src/deltachat/account.py +++ b/python/src/deltachat/account.py @@ -293,6 +293,8 @@ def create_contact(self, obj, name: Optional[str] = None) -> Contact: return Contact(self, contact_id) def get_contact(self, obj) -> Optional[Contact]: + if isinstance(obj, Account): + return self.create_contact(obj) if isinstance(obj, Contact): return obj (_, addr) = self.get_contact_addr_and_name(obj) From 1acedaaf04ccdad526d3dea6e25ec63d77b5ca64 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 20:03:00 +0000 Subject: [PATCH 177/381] fix test_removing_blocked_user_from_group --- python/tests/test_3_offline.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/python/tests/test_3_offline.py b/python/tests/test_3_offline.py index a14deadce2..285fa771ea 100644 --- a/python/tests/test_3_offline.py +++ b/python/tests/test_3_offline.py @@ -318,13 +318,14 @@ def test_group_chat_qr(self, acfactory, ac1, verified): qr = chat.get_join_qr() assert ac2.check_qr(qr).is_ask_verifygroup - def test_removing_blocked_user_from_group(self, ac1, lp): + def test_removing_blocked_user_from_group(self, ac1, acfactory, lp): """ Test that blocked contact is not unblocked when removed from a group. See https://github.com/deltachat/deltachat-core-rust/issues/2030 """ lp.sec("Create a group chat with a contact") - contact = ac1.create_contact("some1@example.org") + ac2 = acfactory.get_pseudo_configured_account() + contact = ac1.create_contact(ac2) group = ac1.create_group_chat("title", contacts=[contact]) group.send_text("First group message") @@ -336,10 +337,6 @@ def test_removing_blocked_user_from_group(self, ac1, lp): group.remove_contact(contact) assert contact.is_blocked() - lp.sec("ac1 adding blocked contact unblocks it") - group.add_contact(contact) - assert not contact.is_blocked() - def test_get_set_profile_image_simple(self, ac1, data): chat = ac1.create_group_chat(name="title1") p = data.get_path("d.png") From 51e3c47e64c431c18ac78bbe1d0fb04af4c3b97a Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 20:05:14 +0000 Subject: [PATCH 178/381] remove test_add_remove_member_remote_events --- python/tests/test_1_online.py | 73 ----------------------------------- 1 file changed, 73 deletions(-) diff --git a/python/tests/test_1_online.py b/python/tests/test_1_online.py index 98d6dffef0..722d431fc9 100644 --- a/python/tests/test_1_online.py +++ b/python/tests/test_1_online.py @@ -1287,79 +1287,6 @@ def test_set_get_contact_avatar(acfactory, data, lp): assert msg6.get_sender_contact().get_profile_image() is None -def test_add_remove_member_remote_events(acfactory, lp): - ac1, ac2, ac3 = acfactory.get_online_accounts(3) - ac1_addr = ac1.get_config("addr") - ac3_addr = ac3.get_config("addr") - # activate local plugin for ac2 - in_list = queue.Queue() - - class EventHolder: - def __init__(self, **kwargs) -> None: - self.__dict__.update(kwargs) - - class InPlugin: - @account_hookimpl - def ac_incoming_message(self, message): - # we immediately accept the sender because - # otherwise we won't see member_added contacts - message.create_chat() - - @account_hookimpl - def ac_chat_modified(self, chat): - in_list.put(EventHolder(action="chat-modified", chat=chat)) - - @account_hookimpl - def ac_member_added(self, chat, contact, message): - in_list.put(EventHolder(action="added", chat=chat, contact=contact, message=message)) - - @account_hookimpl - def ac_member_removed(self, chat, contact, message): - in_list.put(EventHolder(action="removed", chat=chat, contact=contact, message=message)) - - ac2.add_account_plugin(InPlugin()) - - lp.sec("ac1: create group chat with ac2") - chat = ac1.create_group_chat("hello", contacts=[ac2]) - - lp.sec("ac1: send a message to group chat to promote the group") - chat.send_text("afterwards promoted") - ev = in_list.get() - assert ev.action == "chat-modified" - assert chat.is_promoted() - assert sorted(x.addr for x in chat.get_contacts()) == sorted(x.addr for x in ev.chat.get_contacts()) - - lp.sec("ac1: add address2") - # note that if the above create_chat() would not - # happen we would not receive a proper member_added event - contact2 = chat.add_contact(ac3) - ev = in_list.get() - assert ev.action == "chat-modified" - ev = in_list.get() - assert ev.action == "chat-modified" - ev = in_list.get() - assert ev.action == "added" - assert ev.message.get_sender_contact().addr == ac1_addr - assert ev.contact.addr == ac3_addr - - lp.sec("ac1: remove address2") - chat.remove_contact(contact2) - ev = in_list.get() - assert ev.action == "chat-modified" - ev = in_list.get() - assert ev.action == "removed" - assert ev.contact.addr == contact2.addr - assert ev.message.get_sender_contact().addr == ac1_addr - - lp.sec("ac1: remove ac2 contact from chat") - chat.remove_contact(ac2) - ev = in_list.get() - assert ev.action == "chat-modified" - ev = in_list.get() - assert ev.action == "removed" - assert ev.message.get_sender_contact().addr == ac1_addr - - def test_system_group_msg_from_blocked_user(acfactory, lp): """ Tests that a blocked user removes you from a group. From e4e8bcf31f16e8039f9256776d6f2ecb6dfb11cf Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 20:19:34 +0000 Subject: [PATCH 179/381] cleanup --- src/stock_str.rs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/stock_str.rs b/src/stock_str.rs index dc8a0ab885..25b7f6072b 100644 --- a/src/stock_str.rs +++ b/src/stock_str.rs @@ -842,21 +842,6 @@ pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> St .replace1(addr) } -/// Stock string: `Cannot establish guaranteed end-to-end encryption with %1$s`. -pub(crate) async fn contact_not_verified(context: &Context, contact: &Contact) -> String { - let addr = contact.get_display_name(); - translated(context, StockMessage::ContactNotVerified) - .await - .replace1(addr) -} - -/// Stock string: `Changed setup for %1$s`. -pub(crate) async fn contact_setup_changed(context: &Context, contact_addr: &str) -> String { - translated(context, StockMessage::ContactSetupChanged) - .await - .replace1(contact_addr) -} - /// Stock string: `Archived chats`. pub(crate) async fn archived_chats(context: &Context) -> String { translated(context, StockMessage::ArchivedChats).await From 18f7b1a56906c339d414c39ba1a890cd97b4d764 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 20:19:34 +0000 Subject: [PATCH 180/381] resolve todo --- src/securejoin.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/securejoin.rs b/src/securejoin.rs index 8665650fca..7e19b9e979 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -524,17 +524,25 @@ pub(crate) async fn observe_securejoin_on_other_device( return Ok(HandshakeMessage::Ignore); } - let addr = Contact::get_by_id(context, contact_id) - .await? + let contact = Contact::get_by_id(context, contact_id).await?; + let addr = contact .get_addr() .to_lowercase(); let Some(key) = mime_message.gossiped_keys.get(&addr) else { - // TODO: check that contact_id fingerprint is the same as gossiped key fingerprint + return Ok(HandshakeMessage::Ignore); + }; + let Some(contact_fingerprint) = contact.fingerprint() else { + // Not a PGP-contact, should not happen. return Ok(HandshakeMessage::Ignore); }; + if key.dc_fingerprint() != contact_fingerprint { + // Fingerprint does not match, ignore. + return Ok(HandshakeMessage::Ignore); + } + mark_contact_id_as_verified(context, contact_id, ContactId::SELF).await?; ChatId::set_protection_for_contact(context, contact_id, mime_message.timestamp_sent).await?; From 1028fa2643cce2778aaa58687e8f23ab90b59497 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 20:33:28 +0000 Subject: [PATCH 181/381] remove aeap_addr_changed --- src/stock_str.rs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/stock_str.rs b/src/stock_str.rs index 25b7f6072b..fc5313dd63 100644 --- a/src/stock_str.rs +++ b/src/stock_str.rs @@ -1249,20 +1249,6 @@ pub(crate) async fn broadcast_list(context: &Context) -> String { translated(context, StockMessage::BroadcastList).await } -/// Stock string: `%1$s changed their address from %2$s to %3$s`. -pub(crate) async fn aeap_addr_changed( - context: &Context, - contact_name: &str, - old_addr: &str, - new_addr: &str, -) -> String { - translated(context, StockMessage::AeapAddrChanged) - .await - .replace1(contact_name) - .replace2(old_addr) - .replace3(new_addr) -} - /// Stock string: `⚠️ Your email provider %1$s requires end-to-end encryption which is not setup yet. Tap to learn more.`. pub(crate) async fn unencrypted_email(context: &Context, provider: &str) -> String { translated(context, StockMessage::InvalidUnencryptedMail) From 7bfe247b828207a1b7244507432febbdf32b4b14 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 20:49:25 +0000 Subject: [PATCH 182/381] fixes --- src/contact/contact_tests.rs | 23 ++++++++++++++------ src/receive_imf.rs | 41 ++++++++++++++++++++++++++++-------- src/securejoin.rs | 4 +--- 3 files changed, 50 insertions(+), 18 deletions(-) diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs index 1a5ab7b957..05c82ffa26 100644 --- a/src/contact/contact_tests.rs +++ b/src/contact/contact_tests.rs @@ -1217,14 +1217,18 @@ async fn test_name_changes() -> Result<()> { let alice = &tcm.alice().await; let bob = &tcm.bob().await; - alice.set_config(Config::Displayname, Some("Alice Revision 1")).await?; + alice + .set_config(Config::Displayname, Some("Alice Revision 1")) + .await?; let alice_bob_chat = alice.create_chat(bob).await; let sent_msg = alice.send_text(alice_bob_chat.id, "Hello").await; let bob_alice_id = bob.recv_msg(&sent_msg).await.from_id; let bob_alice_contact = Contact::get_by_id(bob, bob_alice_id).await?; assert_eq!(bob_alice_contact.get_display_name(), "Alice Revision 1"); - alice.set_config(Config::Displayname, Some("Alice Revision 2")).await?; + alice + .set_config(Config::Displayname, Some("Alice Revision 2")) + .await?; let sent_msg = alice.send_text(alice_bob_chat.id, "Hello").await; bob.recv_msg(&sent_msg).await; let bob_alice_contact = Contact::get_by_id(bob, bob_alice_id).await?; @@ -1235,14 +1239,19 @@ async fn test_name_changes() -> Result<()> { bob_alice_contact.id.set_name(bob, "Renamed").await?; let event = bob .evtracker - .get_matching(|e| matches!(e, EventType::ContactsChanged {..})) + .get_matching(|e| matches!(e, EventType::ContactsChanged { .. })) .await; - assert_eq!(event, EventType::ContactsChanged(Some(bob_alice_contact.id))); + assert_eq!( + event, + EventType::ContactsChanged(Some(bob_alice_contact.id)) + ); let bob_alice_contact = Contact::get_by_id(bob, bob_alice_id).await?; assert_eq!(bob_alice_contact.get_display_name(), "Renamed"); // Alice also renames self into "Renamed". - alice.set_config(Config::Displayname, Some("Renamed")).await?; + alice + .set_config(Config::Displayname, Some("Renamed")) + .await?; let sent_msg = alice.send_text(alice_bob_chat.id, "Hello").await; bob.recv_msg(&sent_msg).await; let bob_alice_contact = Contact::get_by_id(bob, bob_alice_id).await?; @@ -1250,7 +1259,9 @@ async fn test_name_changes() -> Result<()> { // Contact name was set to "Renamed" explicitly before, // so it should not be changed. - alice.set_config(Config::Displayname, Some("Renamed again")).await?; + alice + .set_config(Config::Displayname, Some("Renamed again")) + .await?; let sent_msg = alice.send_text(alice_bob_chat.id, "Hello").await; bob.recv_msg(&sent_msg).await; let bob_alice_contact = Contact::get_by_id(bob, bob_alice_id).await?; diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 68b014119c..090a7eefa5 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -2055,8 +2055,7 @@ async fn lookup_chat_or_create_adhoc_group( create_blocked: Blocked, is_partial_download: bool, ) -> Result> { - // FIXME - let to_ids: Vec = to_ids.iter().copied().filter_map(|x| x).collect(); + let to_ids: Vec = to_ids.iter().filter_map(|x| *x).collect(); if let Some((new_chat_id, new_chat_id_blocked)) = // Try to assign to a chat based on In-Reply-To/References. @@ -2204,7 +2203,7 @@ async fn create_group( verified_encryption: &VerifiedEncryption, grpid: &str, ) -> Result> { - let to_ids_flat: Vec = to_ids.iter().copied().filter_map(|x| x).collect(); + let to_ids_flat: Vec = to_ids.iter().filter_map(|x| *x).collect(); let mut chat_id = None; let mut chat_id_blocked = Default::default(); @@ -2439,8 +2438,7 @@ async fn apply_group_changes( past_ids: &[Option], verified_encryption: &VerifiedEncryption, ) -> Result { - // FIXME - let to_ids_flat: Vec = to_ids.iter().copied().filter_map(|x| x).collect(); + let to_ids_flat: Vec = to_ids.iter().filter_map(|x| *x).collect(); if chat_id.is_special() { // Do not apply group changes to the trash chat. return Ok(GroupChangesInfo::default()); @@ -3342,6 +3340,7 @@ async fn lookup_pgp_contact_by_fingerprint( /// Looks up PGP-contacts by email addresses. /// +/// `fingerprints` may be empty. /// This is used as a fallback when email addresses are available, /// but not the fingerprints, e.g. when core 1.157.3 /// client sends the `To` and `Chat-Group-Past-Members` header @@ -3360,14 +3359,38 @@ async fn lookup_pgp_contacts_by_address_list( fingerprints: &[Fingerprint], chat_id: ChatId, ) -> Result>> { - // TODO: create a contact with a given fingerprint - // if fingerprint is available. let mut contact_ids = Vec::new(); + let mut fingerprint_iter = fingerprints.iter(); for info in address_list { let addr = &info.addr; + if !may_be_valid_addr(addr) { + contact_ids.push(None); + continue; + } + + if let Some(fp) = fingerprint_iter.next() { + // Iterator has not ran out of fingerprints yet. + let display_name = info.display_name.as_deref(); + let fingerprint: String = fp.hex(); - let contact_id = lookup_pgp_contact_by_address(context, addr, chat_id).await?; - contact_ids.push(contact_id); + if let Ok(addr) = ContactAddress::new(addr) { + let (contact_id, _) = Contact::add_or_lookup_ex( + context, + display_name.unwrap_or_default(), + &addr, + &fingerprint, + Origin::Hidden, + ) + .await?; + contact_ids.push(Some(contact_id)); + } else { + warn!(context, "Contact with address {:?} cannot exist.", addr); + contact_ids.push(None); + } + } else { + let contact_id = lookup_pgp_contact_by_address(context, addr, chat_id).await?; + contact_ids.push(contact_id); + } } debug_assert_eq!(address_list.len(), contact_ids.len()); Ok(contact_ids) diff --git a/src/securejoin.rs b/src/securejoin.rs index 7e19b9e979..19b47df8df 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -525,9 +525,7 @@ pub(crate) async fn observe_securejoin_on_other_device( } let contact = Contact::get_by_id(context, contact_id).await?; - let addr = contact - .get_addr() - .to_lowercase(); + let addr = contact.get_addr().to_lowercase(); let Some(key) = mime_message.gossiped_keys.get(&addr) else { return Ok(HandshakeMessage::Ignore); From d46bda1eb023ed24f4f6b1ca7729b448e7964126 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 20:50:55 +0000 Subject: [PATCH 183/381] format python --- python/src/deltachat/chat.py | 2 +- python/tests/test_3_offline.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/python/src/deltachat/chat.py b/python/src/deltachat/chat.py index 8c06c4d648..edc2d5ba7d 100644 --- a/python/src/deltachat/chat.py +++ b/python/src/deltachat/chat.py @@ -423,7 +423,7 @@ def add_contact(self, obj): contact = obj else: contact = self.account.create_contact(obj) - + ret = lib.dc_add_contact_to_chat(self.account._dc_context, self.id, contact.id) if ret != 1: raise ValueError(f"could not add contact {contact!r} to chat") diff --git a/python/tests/test_3_offline.py b/python/tests/test_3_offline.py index 285fa771ea..8e1323e3a5 100644 --- a/python/tests/test_3_offline.py +++ b/python/tests/test_3_offline.py @@ -200,8 +200,8 @@ def test_delete_referenced_contact_hides_contact(self, acfactory): def test_create_chat_flexibility(self, acfactory): ac1 = acfactory.get_pseudo_configured_account() ac2 = acfactory.get_pseudo_configured_account() - chat1 = ac1.create_chat(ac2) # This creates a PGP-contact chat - chat2 = ac1.create_chat(ac2.get_self_contact().addr) # This creates email-contact chat + chat1 = ac1.create_chat(ac2) # This creates a PGP-contact chat + chat2 = ac1.create_chat(ac2.get_self_contact().addr) # This creates email-contact chat assert chat1 != chat2 ac3 = acfactory.get_unconfigured_account() with pytest.raises(ValueError): From ec91fc065a10a6d35faeb49b4d1e5679c4d7205e Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 21:17:37 +0000 Subject: [PATCH 184/381] fix thunderbird tests and clippy --- deltachat-repl/src/cmdline.rs | 3 +- src/receive_imf/receive_imf_tests.rs | 66 ++++++++++++++++++++++++---- 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/deltachat-repl/src/cmdline.rs b/deltachat-repl/src/cmdline.rs index 36dbd386ac..d00cf65708 100644 --- a/deltachat-repl/src/cmdline.rs +++ b/deltachat-repl/src/cmdline.rs @@ -20,7 +20,6 @@ use deltachat::log::LogExt; use deltachat::message::{self, Message, MessageState, MsgId, Viewtype}; use deltachat::mimeparser::SystemMessage; use deltachat::peer_channels::{send_webxdc_realtime_advertisement, send_webxdc_realtime_data}; -use deltachat::peerstate::*; use deltachat::qr::*; use deltachat::qr_code_generator::create_qr_svg; use deltachat::reaction::send_reaction; @@ -269,7 +268,7 @@ async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<()> { async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()> { for contact_id in contacts { - let mut line2 = "".to_string(); + let line2 = "".to_string(); let contact = Contact::get_by_id(context, *contact_id).await?; let name = contact.get_display_name(); let addr = contact.get_addr(); diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index ba4ffea679..5959b7f847 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -3249,31 +3249,76 @@ async fn test_outgoing_undecryptable() -> Result<()> { Ok(()) } +/// Tests that a message from Thunderbird with an Autocrypt header is assigned to the PGP-contact. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_thunderbird_autocrypt() -> Result<()> { let t = TestContext::new_bob().await; let raw = include_bytes!("../../test-data/message/thunderbird_with_autocrypt.eml"); let received_msg = receive_imf(&t, raw, false).await?.unwrap(); + + assert_eq!(received_msg.msg_ids.len(), 1); + let msg_id = received_msg.msg_ids[0]; - // TODO: if the message should arrive as PGP-contact - // with the key taken from Autocrypt header. + let message = Message::load_from_db(&t, msg_id).await?; + assert!(message.get_showpadlock()); + + let from_id = message.from_id; + + let from_contact = Contact::get_by_id(&t, from_id).await?; + assert!(from_contact.is_pgp_contact()); Ok(()) } +/// Tests reception of a message from Thunderbird with attached key. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_prefer_encrypt_mutual_if_encrypted() -> Result<()> { let t = TestContext::new_bob().await; + // The message has public key attached *and* Autocrypt header. + // + // Autocrypt header is used to check the signature. + // + // At the time of the writing (2025-04-30, introduction of PGP-contacts) + // signature checking does not work without the Autocrypt header. let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_signed_with_pubkey.eml"); - receive_imf(&t, raw, false).await?; - // TODO: the message should arrive as PGP-contact with a key. + let received_msg = receive_imf(&t, raw, false).await?.unwrap(); + + // Attached key does not appear as an attachment, + // there is only one part. + assert_eq!(received_msg.msg_ids.len(), 1); + let msg_id = received_msg.msg_ids[0]; + + let message = Message::load_from_db(&t, msg_id).await?; + assert!(message.get_showpadlock()); + let alice_id = message.from_id; + let alice_contact = Contact::get_by_id(&t, alice_id).await?; + assert!(alice_contact.is_pgp_contact()); + + // The message without the Autocrypt header + // cannot be assigned to the contact even if it + // is encrypted and signed. + // + // This could be fixed by looking up + // the key by the issuer fingerprint + // which is present in the detached signature, + // but this is not done yet. let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_signed.eml"); - receive_imf(&t, raw, false).await?; - // TODO: the message should arrive as the same PGP-contact. + let received_msg = receive_imf(&t, raw, false).await?.unwrap(); + + assert_eq!(received_msg.msg_ids.len(), 1); + let msg_id = received_msg.msg_ids[0]; + + let message = Message::load_from_db(&t, msg_id).await?; + assert!(!message.get_showpadlock()); + + let alice_email_id = message.from_id; + assert_ne!(alice_email_id, alice_id); + let alice_email_contact = Contact::get_by_id(&t, alice_email_id).await?; + assert!(!alice_email_contact.is_pgp_contact()); Ok(()) } @@ -3283,7 +3328,10 @@ async fn test_forged_from_and_no_valid_signatures() -> Result<()> { let t = &TestContext::new_bob().await; let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_signed.eml"); let received_msg = receive_imf(t, raw, false).await?.unwrap(); - let msg = t.get_last_msg().await; + + assert_eq!(received_msg.msg_ids.len(), 1); + let msg_id = received_msg.msg_ids[0]; + let msg = Message::load_from_db(&t, msg_id).await?; assert!(!msg.chat_id.is_trash()); assert!(!msg.get_showpadlock()); @@ -3300,7 +3348,7 @@ async fn test_wrong_from_name_and_no_valid_signatures() -> Result<()> { let t = &TestContext::new_bob().await; let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_signed.eml"); let raw = String::from_utf8(raw.to_vec())?.replace("From: Alice", "From: A"); - let received_msg = receive_imf(t, raw.as_bytes(), false).await?.unwrap(); + receive_imf(t, raw.as_bytes(), false).await?.unwrap(); let msg = t.get_last_msg().await; assert!(!msg.chat_id.is_trash()); assert!(!msg.get_showpadlock()); @@ -3337,7 +3385,7 @@ async fn test_thunderbird_unsigned() -> Result<()> { // Alice receives an unsigned message from Bob. let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_unsigned.eml"); - let received_msg = receive_imf(&alice, raw, false).await?.unwrap(); + receive_imf(&alice, raw, false).await?.unwrap(); let msg = alice.get_last_msg().await; assert!(!msg.get_showpadlock()); From 9a79edc6a4fcbd566c87f43b2857b72cdc1b3869 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 21:20:27 +0000 Subject: [PATCH 185/381] make clippy happy --- src/receive_imf/receive_imf_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 5959b7f847..0d9f439396 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -3331,7 +3331,7 @@ async fn test_forged_from_and_no_valid_signatures() -> Result<()> { assert_eq!(received_msg.msg_ids.len(), 1); let msg_id = received_msg.msg_ids[0]; - let msg = Message::load_from_db(&t, msg_id).await?; + let msg = Message::load_from_db(t, msg_id).await?; assert!(!msg.chat_id.is_trash()); assert!(!msg.get_showpadlock()); From d71d9a91ecb9f16f18f8b1a5619da72edd7056b4 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 21:21:39 +0000 Subject: [PATCH 186/381] python lint --- python/tests/test_3_offline.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/python/tests/test_3_offline.py b/python/tests/test_3_offline.py index 8e1323e3a5..03ca1b8e23 100644 --- a/python/tests/test_3_offline.py +++ b/python/tests/test_3_offline.py @@ -1,12 +1,11 @@ import os -import time from datetime import datetime, timedelta, timezone import pytest import deltachat as dc from deltachat.tracker import ImexFailed -from deltachat import Account, account_hookimpl, Message +from deltachat import Account, Message @pytest.mark.parametrize( From 2e5664c047fa5352dc61477225baf8bb8fc8d3f5 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 21:28:30 +0000 Subject: [PATCH 187/381] rustfmt --- src/receive_imf/receive_imf_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 0d9f439396..946b8c62c9 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -3256,7 +3256,7 @@ async fn test_thunderbird_autocrypt() -> Result<()> { let raw = include_bytes!("../../test-data/message/thunderbird_with_autocrypt.eml"); let received_msg = receive_imf(&t, raw, false).await?.unwrap(); - + assert_eq!(received_msg.msg_ids.len(), 1); let msg_id = received_msg.msg_ids[0]; From b839ba49a31ccde4653e1f265c091caf4ee20f62 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 21:28:51 +0000 Subject: [PATCH 188/381] python formatting --- deltachat-rpc-client/tests/test_something.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index 71f13a127b..8202949034 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -170,7 +170,7 @@ def test_account(acfactory) -> None: assert alice.get_size() assert alice.is_configured() assert not alice.get_avatar() - assert alice.get_contact_by_addr(bob_addr) == None # There is no email-contact, only PGP-contact + assert alice.get_contact_by_addr(bob_addr) == None # There is no email-contact, only PGP-contact assert alice.get_contacts() assert alice.get_contacts(snapshot=True) assert alice.self_contact From 29e0e3ccb96938798439414230ea015cb99b9af2 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Apr 2025 21:33:36 +0000 Subject: [PATCH 189/381] python lint --- deltachat-rpc-client/tests/test_securejoin.py | 1 - deltachat-rpc-client/tests/test_something.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/deltachat-rpc-client/tests/test_securejoin.py b/deltachat-rpc-client/tests/test_securejoin.py index 5582f87c1e..0a49a7ad88 100644 --- a/deltachat-rpc-client/tests/test_securejoin.py +++ b/deltachat-rpc-client/tests/test_securejoin.py @@ -1,5 +1,4 @@ import logging -import time import pytest diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index 8202949034..f538699c68 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -170,7 +170,7 @@ def test_account(acfactory) -> None: assert alice.get_size() assert alice.is_configured() assert not alice.get_avatar() - assert alice.get_contact_by_addr(bob_addr) == None # There is no email-contact, only PGP-contact + assert alice.get_contact_by_addr(bob_addr) is None # There is no email-contact, only PGP-contact assert alice.get_contacts() assert alice.get_contacts(snapshot=True) assert alice.self_contact From 4f4e22fc01d56485f3a8baea0fb0493eadad7a59 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 2 May 2025 17:04:34 +0000 Subject: [PATCH 190/381] remove group tracking not working with pgp-contacts --- python/examples/group_tracking.py | 49 --------------------------- python/examples/test_examples.py | 55 ------------------------------- python/src/deltachat/events.py | 11 ++----- python/src/deltachat/message.py | 54 ------------------------------ 4 files changed, 2 insertions(+), 167 deletions(-) delete mode 100644 python/examples/group_tracking.py diff --git a/python/examples/group_tracking.py b/python/examples/group_tracking.py deleted file mode 100644 index 51f63cb743..0000000000 --- a/python/examples/group_tracking.py +++ /dev/null @@ -1,49 +0,0 @@ -# content of group_tracking.py - -from deltachat import account_hookimpl, run_cmdline - - -class GroupTrackingPlugin: - @account_hookimpl - def ac_incoming_message(self, message): - print("process_incoming message", message) - if message.text.strip() == "/quit": - message.account.shutdown() - else: - # unconditionally accept the chat - message.create_chat() - addr = message.get_sender_contact().addr - text = message.text - message.chat.send_text(f"echoing from {addr}:\n{text}") - - @account_hookimpl - def ac_outgoing_message(self, message): - print("ac_outgoing_message:", message) - - @account_hookimpl - def ac_configure_completed(self, success): - print("ac_configure_completed:", success) - - @account_hookimpl - def ac_chat_modified(self, chat): - print("ac_chat_modified:", chat.id, chat.get_name()) - for member in chat.get_contacts(): - print(f"chat member: {member.addr}") - - @account_hookimpl - def ac_member_added(self, chat, contact, actor, message): - print(f"ac_member_added {contact.addr} to chat {chat.id} from {actor or message.get_sender_contact().addr}") - for member in chat.get_contacts(): - print(f"chat member: {member.addr}") - - @account_hookimpl - def ac_member_removed(self, chat, contact, actor, message): - print(f"ac_member_removed {contact.addr} from chat {chat.id} by {actor or message.get_sender_contact().addr}") - - -def main(argv=None): - run_cmdline(argv=argv, account_plugins=[GroupTrackingPlugin()]) - - -if __name__ == "__main__": - main() diff --git a/python/examples/test_examples.py b/python/examples/test_examples.py index 7e55d6617c..58fac9c65d 100644 --- a/python/examples/test_examples.py +++ b/python/examples/test_examples.py @@ -1,10 +1,7 @@ import echo_and_quit -import group_tracking import py import pytest -from deltachat.events import FFIEventLogger - @pytest.fixture(scope="session") def datadir(): @@ -36,55 +33,3 @@ def test_echo_quit_plugin(acfactory, lp): lp.sec("send quit sequence") bot_chat.send_text("/quit") botproc.wait() - - -def test_group_tracking_plugin(acfactory, lp): - lp.sec("creating one group-tracking bot and two temp accounts") - botproc = acfactory.run_bot_process(group_tracking) - - ac1, ac2 = acfactory.get_online_accounts(2) - - ac1.add_account_plugin(FFIEventLogger(ac1)) - ac2.add_account_plugin(FFIEventLogger(ac2)) - - lp.sec("creating bot test group with bot") - bot_chat = ac1.qr_setup_contact(botproc.qr) - ac1._evtracker.wait_securejoin_joiner_progress(1000) - bot_contact = bot_chat.get_contacts()[0] - ch = ac1.create_group_chat("bot test group") - ch.add_contact(bot_contact) - ch.send_text("hello") - - botproc.fnmatch_lines( - """ - *ac_chat_modified*bot test group* - """, - ) - - lp.sec("adding third member {}".format(ac2.get_config("addr"))) - contact3 = ac1.create_contact(ac2) - ch.add_contact(contact3) - - reply = ac1._evtracker.wait_next_incoming_message() - assert "hello" in reply.text - - lp.sec("now looking at what the bot received") - botproc.fnmatch_lines( - """ - *ac_member_added {}*from*{}* - """.format( - contact3.addr, - ac1.get_config("addr"), - ), - ) - - lp.sec("contact successfully added, now removing") - ch.remove_contact(contact3) - botproc.fnmatch_lines( - """ - *ac_member_removed {}*from*{}* - """.format( - contact3.addr, - ac1.get_config("addr"), - ), - ) diff --git a/python/src/deltachat/events.py b/python/src/deltachat/events.py index b9fcb4f64c..f559e9fadd 100644 --- a/python/src/deltachat/events.py +++ b/python/src/deltachat/events.py @@ -13,7 +13,6 @@ from .capi import ffi, lib from .cutil import from_optional_dc_charpointer from .hookspec import account_hookimpl -from .message import map_system_message def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}): @@ -304,21 +303,15 @@ def _map_ffi_event(self, ffi_event: FFIEvent): elif name == "DC_EVENT_INCOMING_MSG": msg = account.get_message_by_id(ffi_event.data2) if msg is not None: - yield map_system_message(msg) or ("ac_incoming_message", {"message": msg}) + yield ("ac_incoming_message", {"message": msg}) elif name == "DC_EVENT_MSGS_CHANGED": if ffi_event.data2 != 0: msg = account.get_message_by_id(ffi_event.data2) if msg is not None: if msg.is_outgoing(): - res = map_system_message(msg) - if res and res[0].startswith("ac_member"): - yield res yield "ac_outgoing_message", {"message": msg} elif msg.is_in_fresh(): - yield map_system_message(msg) or ( - "ac_incoming_message", - {"message": msg}, - ) + yield "ac_incoming_message", {"message": msg} elif name == "DC_EVENT_REACTIONS_CHANGED": assert ffi_event.data1 > 0 msg = account.get_message_by_id(ffi_event.data2) diff --git a/python/src/deltachat/message.py b/python/src/deltachat/message.py index d5447d76cd..d15c1696ed 100644 --- a/python/src/deltachat/message.py +++ b/python/src/deltachat/message.py @@ -2,7 +2,6 @@ import json import os -import re from datetime import datetime, timezone from typing import Optional, Union @@ -504,56 +503,3 @@ def get_viewtype_code_from_name(view_type_name): raise ValueError( f"message typecode not found for {view_type_name!r}, available {list(_view_type_mapping.keys())!r}", ) - - -# -# some helper code for turning system messages into hook events -# - - -def map_system_message(msg): - if msg.is_system_message(): - res = parse_system_add_remove(msg.text) - if not res: - return None - action, affected, actor = res - affected = msg.account.get_contact_by_addr(affected) - actor = None if actor == "me" else msg.account.get_contact_by_addr(actor) - d = {"chat": msg.chat, "contact": affected, "actor": actor, "message": msg} - return "ac_member_" + res[0], d - - -def extract_addr(text): - m = re.match(r".*\((.+@.+)\)", text) - if m: - text = m.group(1) - text = text.rstrip(".") - return text.strip() - - -def parse_system_add_remove(text): - """return add/remove info from parsing the given system message text. - - returns a (action, affected, actor) triple - """ - # You removed member a@b. - # You added member a@b. - # Member Me (x@y) removed by a@b. - # Member x@y added by a@b - # Member With space (tmp1@x.org) removed by tmp2@x.org. - # Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).", - # Group left by some one (tmp1@x.org). - # Group left by tmp1@x.org. - text = text.lower() - m = re.match(r"member (.+) (removed|added) by (.+)", text) - if m: - affected, action, actor = m.groups() - return action, extract_addr(affected), extract_addr(actor) - m = re.match(r"you (removed|added) member (.+)", text) - if m: - action, affected = m.groups() - return action, extract_addr(affected), "me" - if text.startswith("group left by "): - addr = extract_addr(text[13:]) - if addr: - return "removed", addr, addr From d50795a5f1ef7072f3c49b738ab87a56f7f3dfd6 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 2 May 2025 17:32:12 +0000 Subject: [PATCH 191/381] fixup --- python/tests/test_3_offline.py | 39 ---------------------------------- 1 file changed, 39 deletions(-) diff --git a/python/tests/test_3_offline.py b/python/tests/test_3_offline.py index 03ca1b8e23..7fbc25ccb3 100644 --- a/python/tests/test_3_offline.py +++ b/python/tests/test_3_offline.py @@ -8,45 +8,6 @@ from deltachat import Account, Message -@pytest.mark.parametrize( - ("msgtext", "res"), - [ - ( - "Member Me (tmp1@x.org) removed by tmp2@x.org.", - ("removed", "tmp1@x.org", "tmp2@x.org"), - ), - ( - "Member With space (tmp1@x.org) removed by tmp2@x.org.", - ("removed", "tmp1@x.org", "tmp2@x.org"), - ), - ( - "Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).", - ("removed", "tmp1@x.org", "tmp2@x.org"), - ), - ( - "Member With space (tmp1@x.org) removed by me", - ("removed", "tmp1@x.org", "me"), - ), - ( - "Group left by some one (tmp1@x.org).", - ("removed", "tmp1@x.org", "tmp1@x.org"), - ), - ("Group left by tmp1@x.org.", ("removed", "tmp1@x.org", "tmp1@x.org")), - ( - "Member tmp1@x.org added by tmp2@x.org.", - ("added", "tmp1@x.org", "tmp2@x.org"), - ), - ("Member nothing bla bla", None), - ("Another unknown system message", None), - ], -) -def test_parse_system_add_remove(msgtext, res): - from deltachat.message import parse_system_add_remove - - out = parse_system_add_remove(msgtext) - assert out == res - - class TestOfflineAccountBasic: def test_wrong_db(self, tmp_path): p = tmp_path / "hello.db" From 3a1ddbadee67144d942c7e454ebe830b47ad0ead Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 6 May 2025 12:30:11 +0000 Subject: [PATCH 192/381] resolve thunderbird tests TODO --- src/receive_imf/receive_imf_tests.rs | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 946b8c62c9..bb9c57be86 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -3359,18 +3359,34 @@ async fn test_wrong_from_name_and_no_valid_signatures() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_thunderbird_autocrypt_unencrypted() -> Result<()> { - let t = TestContext::new_bob().await; + let bob = &TestContext::new_bob().await; + // Thunderbird message with Autocrypt header and a signature, + // but not encrypted. let raw = include_bytes!("../../test-data/message/thunderbird_with_autocrypt_unencrypted.eml"); - receive_imf(&t, raw, false).await?; + let received_msg = receive_imf(bob, raw, false).await?.unwrap(); + + assert_eq!(received_msg.msg_ids.len(), 1); + let msg_id = received_msg.msg_ids[0]; + let msg = Message::load_from_db(bob, msg_id).await?; + assert!(!msg.get_showpadlock()); - // TODO: the message should arrive as email-contact + // The message should arrive as email-contact + let alice_id = msg.from_id; + let alice_contact = Contact::get_by_id(bob, alice_id).await?; + assert!(!alice_contact.is_pgp_contact()); let raw = include_bytes!("../../test-data/message/thunderbird_signed_unencrypted.eml"); - receive_imf(&t, raw, false).await?; + let received_msg = receive_imf(bob, raw, false).await?.unwrap(); + + assert_eq!(received_msg.msg_ids.len(), 1); + let msg_id = received_msg.msg_ids[0]; + let msg = Message::load_from_db(bob, msg_id).await?; + assert!(!msg.get_showpadlock()); - // TODO: the message should arrive as email-contact? - // or PGP-contact, but no padlock + let alice_id = msg.from_id; + let alice_contact = Contact::get_by_id(bob, alice_id).await?; + assert!(!alice_contact.is_pgp_contact()); Ok(()) } From 64538fa84f29344d625a66f30b8fad4f2934b3ef Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 6 May 2025 15:27:37 +0000 Subject: [PATCH 193/381] fix test_profile_data_on_group_leave --- src/chat/chat_tests.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index efc41f54ef..bc18846d82 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -1516,15 +1516,10 @@ async fn test_shall_attach_selfavatar() -> Result<()> { async fn test_profile_data_on_group_leave() -> Result<()> { let mut tcm = TestContextManager::new(); let t = &tcm.alice().await; + let bob = &tcm.bob().await; let chat_id = create_group_chat(t, ProtectionStatus::Unprotected, "foo").await?; - let (contact_id, _) = Contact::add_or_lookup( - t, - "", - &ContactAddress::new("foo@bar.org")?, - Origin::IncomingUnknownTo, - ) - .await?; + let contact_id = t.add_or_lookup_contact_id(bob).await; add_contact_to_chat(t, chat_id, contact_id).await?; send_text_msg(t, chat_id, "populate".to_string()).await?; @@ -1539,7 +1534,8 @@ async fn test_profile_data_on_group_leave() -> Result<()> { remove_contact_from_chat(t, chat_id, ContactId::SELF).await?; let sent_msg = t.pop_sent_msg().await; - assert!(sent_msg.payload().contains("Chat-User-Avatar")); + let msg = bob.parse_msg(&sent_msg).await; + assert!(msg.header_exists(HeaderDef::ChatUserAvatar)); Ok(()) } From 16e3bf125ea38a93b0d3775d02f224ad0383e9ab Mon Sep 17 00:00:00 2001 From: Hocuri Date: Thu, 24 Apr 2025 11:09:16 +0200 Subject: [PATCH 194/381] [WIP] Migration for PGP-contacts --- src/sql/migrations.rs | 159 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 286c730601..063abcee48 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1,7 +1,11 @@ //! Migrations module. +use std::collections::BTreeMap; +use std::time::Instant; + use anyhow::{ensure, Context as _, Result}; use deltachat_contact_tools::EmailAddress; +use pgp::SignedPublicKey; use rusqlite::OptionalExtension; use crate::config::Config; @@ -9,6 +13,7 @@ use crate::configure::EnteredLoginParam; use crate::constants::ShowEmails; use crate::context::Context; use crate::imap; +use crate::key::DcKey; use crate::login_param::ConfiguredLoginParam; use crate::message::MsgId; use crate::provider::get_provider_by_domain; @@ -1224,6 +1229,7 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint); inc_and_check(&mut migration_version, 132)?; if dbversion < migration_version { + let start = Instant::now(); sql.execute_migration_transaction( |transaction| { transaction.execute_batch( @@ -1252,11 +1258,164 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint); SELECT secondary_verified_key_fingerprint, secondary_verified_key FROM acpeerstates; ")?; + // TODO remove this commented-out code + // transaction.execute( + // "INSERT INTO contacts (name, addr, origin, blocked, last_seen, + // authname, param, status, is_bot) + // SELECT c.name, c.addr, c.origin, c.blocked, c.last_seen, + // c.authname, c.param, c.status, c.is_bot + // FROM contacts c + // INNER JOIN peerstates ON c.addr=acpeerstates.addr", () + // )?; + + // Create up to 3 new contacts for every contact that has a peerstate: + // one from the Autocrypt key fingerprint, one from the verified key fingerprint, + // one from the secondary verified key fingerprint. + // In the process, build two maps from old contact id to new contact id: + // one that maps to Autocrypt PGP-contact, one that maps to verified PGP-contact. + let mut autocrypt_pgp_contacts = BTreeMap::new(); + let mut verified_pgp_contacts = BTreeMap::new(); + // This maps from the verified contact to the original contact id of the verifier. + // It can't map to the verified pgp contact id, because at the time of constructing + // this map, not all pgp contacts are in the database. + let mut verifications = BTreeMap::new(); + + let mut load_contacts_stmt = transaction.prepare( + "SELECT c.id, c.name, c.addr, c.origin, c.blocked, c.last_seen, + c.authname, c.param, c.status, c.is_bot, c.selfavatar_sent, + IFNULL(p.public_key, p.gossip_key), + p.verified_key, p.verifier, + p.secondary_verified_key, p.secondary_verifier + FROM contacts c + INNER JOIN acpeerstates p ON c.addr=p.addr" + )?; + let all_email_contacts = load_contacts_stmt.query_map((), + |row| { + let id: i64 = row.get(0)?; + let name: String = row.get(1)?; + let addr: String = row.get(2)?; + let origin: i64 = row.get(3)?; + let blocked: Option = row.get(4)?; + let last_seen: i64 = row.get(5)?; + let authname: String = row.get(6)?; + let param: String = row.get(7)?; + let status: Option = row.get(8)?; + let is_bot: bool = row.get(9)?; + let selfavatar_sent: i64 = row.get(10)?; + let autocrypt_key = row + .get(11) + .ok() + .and_then(|blob: Vec| SignedPublicKey::from_slice(&blob).ok()); + let verified_key = row + .get(12) + .ok() + .and_then(|blob: Vec| SignedPublicKey::from_slice(&blob).ok()); + let verifier: String = row.get(13)?; + let secondary_verified_key = row + .get(12) + .ok() + .and_then(|blob: Vec| SignedPublicKey::from_slice(&blob).ok()); + let secondary_verifier: String = row.get(15)?; + Ok((id, + name, + addr, + origin, + blocked, + last_seen, + authname, + param, + status, + is_bot, + selfavatar_sent, + autocrypt_key, + verified_key, + verifier, + secondary_verified_key, + secondary_verifier)) + })?; + + let mut insert_contact_stmt = transaction.prepare( + "INSERT INTO contacts (name, addr, origin, blocked, last_seen, + authname, param, status, is_bot, selfavatar_sent, fingerprint) + VALUES(?,?,?,?,?,?,?,?,?,?)" + )?; + let mut fingerprint_exists_stmt = transaction.prepare( + "SELECT id FROM contacts WHERE fingerprint=?" + )?; + let mut original_contact_id_from_addr_stmt = transaction.prepare( + "SELECT id FROM contacts WHERE addr=? AND fingerprint=''" + )?; + for row in all_email_contacts { + let ( + original_id, + name, + addr, + origin, + blocked, + last_seen, + authname, + param, + status, + is_bot, + selfavatar_sent, + autocrypt_key, + verified_key, + verifier, + secondary_verified_key, + secondary_verifier + ) = row?; + let mut insert_contact = |key: SignedPublicKey| { + let fingerprint = key.dc_fingerprint().hex(); + if fingerprint_exists_stmt.exists((&fingerprint,))? { + insert_contact_stmt.execute(( + &name, + &addr, + origin, + blocked, + last_seen, + &authname, + ¶m, + &status, + is_bot, + selfavatar_sent, + fingerprint, + )) + } else { + Ok(0) + } + }; + let mut original_contact_id_from_addr = |addr: &str| { + original_contact_id_from_addr_stmt.query_row((addr,), |row| row.get(0)) + }; + + if let Some(autocrypt_key) = autocrypt_key { + insert_contact(autocrypt_key)?; + autocrypt_pgp_contacts.insert(original_id, transaction.last_insert_rowid()); + } + + if let Some(verified_key) = verified_key { + insert_contact(verified_key)?; + let new_id = transaction.last_insert_rowid(); + verified_pgp_contacts.insert(original_id, new_id); + let verifier_id: i64 = original_contact_id_from_addr(&verifier)?; + verifications.insert(new_id, verifier_id); + } + + if let Some(secondary_verified_key) = secondary_verified_key { + insert_contact(secondary_verified_key)?; + let new_id = transaction.last_insert_rowid(); + let verifier_id: i64 = original_contact_id_from_addr(&secondary_verifier)?; + verifications.insert(new_id, verifier_id); + } + } + drop(load_contacts_stmt); + Ok(()) }, migration_version ) .await?; + info!(context, "PGP contacts migration took {:?}", start.elapsed()); } let new_version = sql From e345f97e87f5127325e20123b53ecac0b270a530 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Thu, 24 Apr 2025 17:13:31 +0200 Subject: [PATCH 195/381] Try to implement everything, but of course things still fail --- src/sql/migrations.rs | 533 +++++++++++++++++++++++++++--------------- 1 file changed, 348 insertions(+), 185 deletions(-) diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 063abcee48..0d8d4c7cae 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1,6 +1,7 @@ //! Migrations module. use std::collections::BTreeMap; +use std::collections::BTreeSet; use std::time::Instant; use anyhow::{ensure, Context as _, Result}; @@ -1230,191 +1231,8 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint); inc_and_check(&mut migration_version, 132)?; if dbversion < migration_version { let start = Instant::now(); - sql.execute_migration_transaction( - |transaction| { - transaction.execute_batch( - "ALTER TABLE contacts ADD COLUMN fingerprint TEXT NOT NULL DEFAULT ''; - - -- Verifier is an ID of the verifier contact. - -- 0 if the contact is not verified. - ALTER TABLE contacts ADD COLUMN verifier INTEGER NOT NULL DEFAULT 0; - - CREATE INDEX contacts_fingerprint_index ON contacts (fingerprint); - - CREATE TABLE public_keys ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - fingerprint TEXT NOT NULL UNIQUE, -- Upper-case fingerprint of the key. - public_key BLOB NOT NULL -- Binary key, not ASCII-armored - ) STRICT; - CREATE INDEX public_key_index ON public_keys (fingerprint); - - INSERT INTO public_keys (fingerprint, public_key) - SELECT public_key_fingerprint, public_key FROM acpeerstates; - INSERT OR IGNORE INTO public_keys (fingerprint, public_key) - SELECT gossip_key_fingerprint, gossip_key FROM acpeerstates; - INSERT OR IGNORE INTO public_keys (fingerprint, public_key) - SELECT verified_key_fingerprint, verified_key FROM acpeerstates; - INSERT OR IGNORE INTO public_keys (fingerprint, public_key) - SELECT secondary_verified_key_fingerprint, secondary_verified_key FROM acpeerstates; - ")?; - - // TODO remove this commented-out code - // transaction.execute( - // "INSERT INTO contacts (name, addr, origin, blocked, last_seen, - // authname, param, status, is_bot) - // SELECT c.name, c.addr, c.origin, c.blocked, c.last_seen, - // c.authname, c.param, c.status, c.is_bot - // FROM contacts c - // INNER JOIN peerstates ON c.addr=acpeerstates.addr", () - // )?; - - // Create up to 3 new contacts for every contact that has a peerstate: - // one from the Autocrypt key fingerprint, one from the verified key fingerprint, - // one from the secondary verified key fingerprint. - // In the process, build two maps from old contact id to new contact id: - // one that maps to Autocrypt PGP-contact, one that maps to verified PGP-contact. - let mut autocrypt_pgp_contacts = BTreeMap::new(); - let mut verified_pgp_contacts = BTreeMap::new(); - // This maps from the verified contact to the original contact id of the verifier. - // It can't map to the verified pgp contact id, because at the time of constructing - // this map, not all pgp contacts are in the database. - let mut verifications = BTreeMap::new(); - - let mut load_contacts_stmt = transaction.prepare( - "SELECT c.id, c.name, c.addr, c.origin, c.blocked, c.last_seen, - c.authname, c.param, c.status, c.is_bot, c.selfavatar_sent, - IFNULL(p.public_key, p.gossip_key), - p.verified_key, p.verifier, - p.secondary_verified_key, p.secondary_verifier - FROM contacts c - INNER JOIN acpeerstates p ON c.addr=p.addr" - )?; - let all_email_contacts = load_contacts_stmt.query_map((), - |row| { - let id: i64 = row.get(0)?; - let name: String = row.get(1)?; - let addr: String = row.get(2)?; - let origin: i64 = row.get(3)?; - let blocked: Option = row.get(4)?; - let last_seen: i64 = row.get(5)?; - let authname: String = row.get(6)?; - let param: String = row.get(7)?; - let status: Option = row.get(8)?; - let is_bot: bool = row.get(9)?; - let selfavatar_sent: i64 = row.get(10)?; - let autocrypt_key = row - .get(11) - .ok() - .and_then(|blob: Vec| SignedPublicKey::from_slice(&blob).ok()); - let verified_key = row - .get(12) - .ok() - .and_then(|blob: Vec| SignedPublicKey::from_slice(&blob).ok()); - let verifier: String = row.get(13)?; - let secondary_verified_key = row - .get(12) - .ok() - .and_then(|blob: Vec| SignedPublicKey::from_slice(&blob).ok()); - let secondary_verifier: String = row.get(15)?; - Ok((id, - name, - addr, - origin, - blocked, - last_seen, - authname, - param, - status, - is_bot, - selfavatar_sent, - autocrypt_key, - verified_key, - verifier, - secondary_verified_key, - secondary_verifier)) - })?; - - let mut insert_contact_stmt = transaction.prepare( - "INSERT INTO contacts (name, addr, origin, blocked, last_seen, - authname, param, status, is_bot, selfavatar_sent, fingerprint) - VALUES(?,?,?,?,?,?,?,?,?,?)" - )?; - let mut fingerprint_exists_stmt = transaction.prepare( - "SELECT id FROM contacts WHERE fingerprint=?" - )?; - let mut original_contact_id_from_addr_stmt = transaction.prepare( - "SELECT id FROM contacts WHERE addr=? AND fingerprint=''" - )?; - for row in all_email_contacts { - let ( - original_id, - name, - addr, - origin, - blocked, - last_seen, - authname, - param, - status, - is_bot, - selfavatar_sent, - autocrypt_key, - verified_key, - verifier, - secondary_verified_key, - secondary_verifier - ) = row?; - let mut insert_contact = |key: SignedPublicKey| { - let fingerprint = key.dc_fingerprint().hex(); - if fingerprint_exists_stmt.exists((&fingerprint,))? { - insert_contact_stmt.execute(( - &name, - &addr, - origin, - blocked, - last_seen, - &authname, - ¶m, - &status, - is_bot, - selfavatar_sent, - fingerprint, - )) - } else { - Ok(0) - } - }; - let mut original_contact_id_from_addr = |addr: &str| { - original_contact_id_from_addr_stmt.query_row((addr,), |row| row.get(0)) - }; - - if let Some(autocrypt_key) = autocrypt_key { - insert_contact(autocrypt_key)?; - autocrypt_pgp_contacts.insert(original_id, transaction.last_insert_rowid()); - } - - if let Some(verified_key) = verified_key { - insert_contact(verified_key)?; - let new_id = transaction.last_insert_rowid(); - verified_pgp_contacts.insert(original_id, new_id); - let verifier_id: i64 = original_contact_id_from_addr(&verifier)?; - verifications.insert(new_id, verifier_id); - } - - if let Some(secondary_verified_key) = secondary_verified_key { - insert_contact(secondary_verified_key)?; - let new_id = transaction.last_insert_rowid(); - let verifier_id: i64 = original_contact_id_from_addr(&secondary_verifier)?; - verifications.insert(new_id, verifier_id); - } - } - drop(load_contacts_stmt); - - Ok(()) - }, - migration_version - ) - .await?; + sql.execute_migration_transaction(|t| migrate_pgp_contacts(context, t), migration_version) + .await?; info!(context, "PGP contacts migration took {:?}", start.elapsed()); } @@ -1435,6 +1253,351 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint); Ok((update_icons, disable_server_delete, recode_avatar)) } +// TODO if this takes very long, then it should be multiple transactions +// so that it can be executed in multiple steps +fn migrate_pgp_contacts( + context: &Context, + transaction: &mut rusqlite::Transaction<'_>, +) -> std::result::Result<(), anyhow::Error> { + // =============================== Step 1: =============================== + // Alter tables + transaction.execute_batch( + "ALTER TABLE contacts ADD COLUMN fingerprint TEXT NOT NULL DEFAULT ''; + + -- Verifier is an ID of the verifier contact. + -- 0 if the contact is not verified. + ALTER TABLE contacts ADD COLUMN verifier INTEGER NOT NULL DEFAULT 0; + + CREATE INDEX contacts_fingerprint_index ON contacts (fingerprint); + + CREATE TABLE public_keys ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fingerprint TEXT NOT NULL UNIQUE, -- Upper-case fingerprint of the key. + public_key BLOB NOT NULL -- Binary key, not ASCII-armored + ) STRICT; + CREATE INDEX public_key_index ON public_keys (fingerprint); + + INSERT INTO public_keys (fingerprint, public_key) + SELECT public_key_fingerprint, public_key FROM acpeerstates; + INSERT OR IGNORE INTO public_keys (fingerprint, public_key) + SELECT gossip_key_fingerprint, gossip_key FROM acpeerstates; + INSERT OR IGNORE INTO public_keys (fingerprint, public_key) + SELECT verified_key_fingerprint, verified_key FROM acpeerstates; + INSERT OR IGNORE INTO public_keys (fingerprint, public_key) + SELECT secondary_verified_key_fingerprint, secondary_verified_key FROM acpeerstates; + ", + )?; + + // TODO remove this commented-out code + // transaction.execute( + // "INSERT INTO contacts (name, addr, origin, blocked, last_seen, + // authname, param, status, is_bot) + // SELECT c.name, c.addr, c.origin, c.blocked, c.last_seen, + // c.authname, c.param, c.status, c.is_bot + // FROM contacts c + // INNER JOIN peerstates ON c.addr=acpeerstates.addr", () + // )?; + // =============================== Step 2: =============================== + // Create up to 3 new contacts for every contact that has a peerstate: + // one from the Autocrypt key fingerprint, one from the verified key fingerprint, + // one from the secondary verified key fingerprint. + // In the process, build two maps from old contact id to new contact id: + // one that maps to Autocrypt PGP-contact, one that maps to verified PGP-contact. + let mut autocrypt_pgp_contacts: BTreeMap = BTreeMap::new(); + let mut autocrypt_pgp_contacts_with_reset_peerstate: BTreeMap = BTreeMap::new(); + // TODO should secondary verified PGP keys also go in here? + let mut verified_pgp_contacts: BTreeMap = BTreeMap::new(); + { + // This maps from the verified contact to the original contact id of the verifier. + // It can't map to the verified pgp contact id, because at the time of constructing + // this map, not all pgp contacts are in the database. + // TODO apply verifications + let mut verifications = BTreeMap::new(); + + let mut load_contacts_stmt = transaction.prepare( + "SELECT c.id, c.name, c.addr, c.origin, c.blocked, c.last_seen, + c.authname, c.param, c.status, c.is_bot, c.selfavatar_sent, + IFNULL(p.public_key, p.gossip_key), + p.verified_key, p.verifier, + p.secondary_verified_key, p.secondary_verifier, p.prefer_encrypted + FROM contacts c + INNER JOIN acpeerstates p ON c.addr=p.addr + WHERE c.id > 9", + )?; + + let all_email_contacts = load_contacts_stmt.query_map((), |row| { + let id: i64 = row.get(0)?; + let name: String = row.get(1)?; + let addr: String = row.get(2)?; + let origin: i64 = row.get(3)?; + let blocked: Option = row.get(4)?; + let last_seen: i64 = row.get(5)?; + let authname: String = row.get(6)?; + let param: String = row.get(7)?; + let status: Option = row.get(8)?; + let is_bot: bool = row.get(9)?; + let selfavatar_sent: i64 = row.get(10)?; + let autocrypt_key = row + .get(11) + .ok() + .and_then(|blob: Vec| SignedPublicKey::from_slice(&blob).ok()); + let verified_key = row + .get(12) + .ok() + .and_then(|blob: Vec| SignedPublicKey::from_slice(&blob).ok()); + let verifier: String = row.get(13)?; + let secondary_verified_key = row + .get(12) + .ok() + .and_then(|blob: Vec| SignedPublicKey::from_slice(&blob).ok()); + let secondary_verifier: String = row.get(15)?; + let prefer_encrypt: u8 = row.get(16)?; + Ok(( + id, + name, + addr, + origin, + blocked, + last_seen, + authname, + param, + status, + is_bot, + selfavatar_sent, + autocrypt_key, + verified_key, + verifier, + secondary_verified_key, + secondary_verifier, + prefer_encrypt, + )) + })?; + + let mut insert_contact_stmt = transaction.prepare( + "INSERT INTO contacts (name, addr, origin, blocked, last_seen, + authname, param, status, is_bot, selfavatar_sent, fingerprint) + VALUES(?,?,?,?,?,?,?,?,?,?,?)", + )?; + let mut fingerprint_to_id_stmt = + transaction.prepare("SELECT id FROM contacts WHERE fingerprint=?")?; + let mut original_contact_id_from_addr_stmt = + transaction.prepare("SELECT id FROM contacts WHERE addr=? AND fingerprint=''")?; + + for row in all_email_contacts { + let ( + original_id, + name, + addr, + origin, + blocked, + last_seen, + authname, + param, + status, + is_bot, + selfavatar_sent, + autocrypt_key, + verified_key, + verifier, + secondary_verified_key, + secondary_verifier, + prefer_encrypt, + ) = row?; + let mut insert_contact = |key: SignedPublicKey| -> Result { + let fingerprint = key.dc_fingerprint().hex(); + let existing_contact_id: Option = fingerprint_to_id_stmt + .query_row((&fingerprint,), |row| row.get(0)) + .optional()?; + if let Some(existing_contact_id) = existing_contact_id { + return Ok(existing_contact_id); + } + insert_contact_stmt.execute(( + &name, + &addr, + origin, + blocked, + last_seen, + &authname, + ¶m, + &status, + is_bot, + selfavatar_sent, + fingerprint, + ))?; + Ok(transaction.last_insert_rowid().try_into()?) + }; + let mut original_contact_id_from_addr = |addr: &str| { + original_contact_id_from_addr_stmt.query_row((addr,), |row| row.get(0)) + }; + + let Some(autocrypt_key) = autocrypt_key else { + continue; + }; + let new_id = insert_contact(autocrypt_key)?; + + // prefer_encrypt == 20 would mean EncryptPreference::Reset, + // i.e. we shouldn't encrypt if possible. + if prefer_encrypt != 20 { + autocrypt_pgp_contacts.insert(original_id.try_into()?, new_id); + } else { + autocrypt_pgp_contacts_with_reset_peerstate.insert(original_id.try_into()?, new_id); + } + + let Some(verified_key) = verified_key else { + continue; + }; + let new_id = insert_contact(verified_key)?; + verified_pgp_contacts.insert(original_id.try_into()?, new_id); + let verifier_id: i64 = original_contact_id_from_addr(&verifier)?; + verifications.insert(new_id, verifier_id); + + let Some(secondary_verified_key) = secondary_verified_key else { + continue; + }; + let new_id = insert_contact(secondary_verified_key)?; + let verifier_id: i64 = original_contact_id_from_addr(&secondary_verifier)?; + // Only use secondary verification if there is no primary verification: + verifications.entry(new_id).or_insert(verifier_id); + } + } + + // ======================= Step 3: ======================= + // For each chat, modify the memberlist to retain the correct contacts + // In the process, track the set of contacts which remained no any chat at all + // in a `BTreeSet`, which initially contains all contact ids + let mut orphaned_contacts: BTreeSet = transaction + .prepare("SELECT id FROM contacts")? + .query_map((), |row| row.get::(0))? + .collect::, rusqlite::Error>>()?; + + { + let mut stmt = transaction.prepare( + "SELECT c.id, c.type, c.grpid, c.protected + FROM chats c", + )?; + let mut load_chat_contacts_stmt = + transaction.prepare("SELECT contact_id FROM chats_contacts WHERE chat_id=?")?; + let all_chats = stmt.query_map((), |row| { + let id: u32 = row.get(0)?; + let typ: u32 = row.get(1)?; + let grpid: String = row.get(2)?; + let protected: u32 = row.get(3)?; + Ok((id, typ, grpid, protected)) + })?; + + for chat in all_chats { + let (chat_id, typ, grpid, protected) = chat?; + // In groups, this also contains past members + let old_members: Vec = load_chat_contacts_stmt + .query_map((chat_id,), |row| row.get::<_, u32>(0))? + .collect::, rusqlite::Error>>()?; + + let mut keep_email_contacts = || { + for m in &old_members { + orphaned_contacts.remove(m); + } + }; + + let old_and_new_members = match typ { + // 1:1 chats retain: + // - email-contact if peerstate is in the "reset" state. + // - PGP-contact identified by the Autocrypt key if Autocrypt key does not match the verified key. + // - PGP-contact identified by the verified key if peerstate Autocrypt key matches the Verified key. + // Since the autocrypt and verified PGP contact are identital in this case, we can add the AutocryptPgp contact, + // and the effect will be the same. + 100 => { + let Some(old_member) = old_members.first() else { + warn!(context, "1:1 chat doesn't contain contact"); + continue; + }; + let Some(&new_contact) = autocrypt_pgp_contacts.get(old_member) else { + // No peerstate, or peerstate in "reset" state. Keep email contact. + keep_email_contacts(); + continue; + }; + vec![(*old_member, Some(new_contact))] + } + + // Group + 120 => { + if grpid.is_empty() { + // Ad-hoc group that has empty Chat-Group-ID + // because it was created in response to receiving a non-chat email. + keep_email_contacts(); + continue; + } else if protected == 1 { + old_members + .iter() + .map(|old_member| { + (*old_member, verified_pgp_contacts.get(old_member).copied()) + }) + .collect() + } else { + old_members + .iter() + .map(|original| { + ( + *original, + autocrypt_pgp_contacts + .get(original) + // TODO it's unclear whether we want to do this: + // We could also make the group unencrypted + // if any peerstate is reset. + .or_else(|| { + autocrypt_pgp_contacts_with_reset_peerstate + .get(original) + }) + .copied(), + ) + }) + .collect() + } + } + + // Mailinglist | Broadcast list + 140 | 160 => { + keep_email_contacts(); + continue; + } + + _ => { + warn!(context, "Invalid chat type {typ}"); + continue; + } + }; + + if old_and_new_members.is_empty() { + keep_email_contacts(); + continue; + } + + for (old_member, new_member) in old_and_new_members { + if let Some(new_member) = new_member { + orphaned_contacts.remove(&new_member); + transaction.execute( + "UPDATE chats_contacts SET contact_id=? WHERE contact_id=? AND chat_id=?", + (new_member, old_member, chat_id), + )?; + } else { + transaction.execute( + "DELETE FROM chats_contacts WHERE contact_id=? AND chat_id=?", + (old_member, chat_id), + )?; + } + } + } + } + + // ======================= Step 4: ======================= + // Mark all contacts which remained in no chat at all as hidden + let mut mark_as_hidden_stmt = transaction.prepare("UPDATE contacts SET origin=? WHERE id=?")?; + for contact in orphaned_contacts { + mark_as_hidden_stmt.execute((0x8, contact))?; + } + + Ok(()) +} + impl Sql { async fn set_db_version(&self, version: i32) -> Result<()> { self.set_raw_config_int(VERSION_CFG, version).await?; From 8fceae4acfaf97e474290f40e852fc10fd042c44 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Fri, 25 Apr 2025 15:32:34 +0200 Subject: [PATCH 196/381] Add a test and make it pass --- Cargo.toml | 2 +- src/sql/migrations.rs | 99 ++++++++++++++++---------- src/sql/migrations/migrations_tests.rs | 67 +++++++++++++++++ 3 files changed, 131 insertions(+), 37 deletions(-) create mode 100644 src/sql/migrations/migrations_tests.rs diff --git a/Cargo.toml b/Cargo.toml index 2696995f0c..a58a89d66b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ opt-level = 1 # Make anyhow `backtrace` feature useful. # With `debug = 0` there are no line numbers in the backtrace # produced with RUST_BACKTRACE=1. -debug = 1 +debug = 'full' opt-level = 0 # Always optimize dependencies. diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 0d8d4c7cae..9dcfc611ee 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -25,6 +25,11 @@ const DBVERSION: i32 = 68; const VERSION_CFG: &str = "dbversion"; const TABLES: &str = include_str!("./tables.sql"); +#[cfg(test)] +tokio::task_local! { + static STOP_MIGRATIONS_AT: i32; +} + pub async fn run(context: &Context, sql: &Sql) -> Result<(bool, bool, bool)> { let mut exists_before_update = false; let mut dbversion_before_update = DBVERSION; @@ -1259,6 +1264,7 @@ fn migrate_pgp_contacts( context: &Context, transaction: &mut rusqlite::Transaction<'_>, ) -> std::result::Result<(), anyhow::Error> { + info!(context, "Starting PGP contact transition."); // =============================== Step 1: =============================== // Alter tables transaction.execute_batch( @@ -1278,14 +1284,20 @@ fn migrate_pgp_contacts( CREATE INDEX public_key_index ON public_keys (fingerprint); INSERT INTO public_keys (fingerprint, public_key) - SELECT public_key_fingerprint, public_key FROM acpeerstates; + SELECT public_key_fingerprint, public_key FROM acpeerstates + WHERE public_key_fingerprint IS NOT NULL AND public_key IS NOT NULL; + INSERT OR IGNORE INTO public_keys (fingerprint, public_key) - SELECT gossip_key_fingerprint, gossip_key FROM acpeerstates; + SELECT gossip_key_fingerprint, gossip_key FROM acpeerstates + WHERE gossip_key_fingerprint IS NOT NULL AND gossip_key IS NOT NULL; + INSERT OR IGNORE INTO public_keys (fingerprint, public_key) - SELECT verified_key_fingerprint, verified_key FROM acpeerstates; + SELECT verified_key_fingerprint, verified_key FROM acpeerstates + WHERE verified_key_fingerprint IS NOT NULL AND verified_key IS NOT NULL; + INSERT OR IGNORE INTO public_keys (fingerprint, public_key) - SELECT secondary_verified_key_fingerprint, secondary_verified_key FROM acpeerstates; - ", + SELECT secondary_verified_key_fingerprint, secondary_verified_key FROM acpeerstates + WHERE secondary_verified_key_fingerprint IS NOT NULL AND secondary_verified_key IS NOT NULL;", )?; // TODO remove this commented-out code @@ -1422,9 +1434,14 @@ fn migrate_pgp_contacts( &status, is_bot, selfavatar_sent, - fingerprint, + fingerprint.clone(), ))?; - Ok(transaction.last_insert_rowid().try_into()?) + let id = transaction.last_insert_rowid().try_into()?; + info!( + context, + "Inserted new contact id={id} name={name} addr={addr} fingerprint={fingerprint}" + ); + Ok(id) }; let mut original_contact_id_from_addr = |addr: &str| { original_contact_id_from_addr_stmt.query_row((addr,), |row| row.get(0)) @@ -1459,6 +1476,16 @@ fn migrate_pgp_contacts( // Only use secondary verification if there is no primary verification: verifications.entry(new_id).or_insert(verifier_id); } + info!( + context, + "Created PGP contacts identified by autocrypt key: {autocrypt_pgp_contacts:?}" + ); + info!(context, "Created PGP contacts with 'reset' peerstate identified by autocrypt key: {autocrypt_pgp_contacts_with_reset_peerstate:?}"); + info!( + context, + "Created PGP contacts identified by verified key: {verified_pgp_contacts:?}" + ); + info!(context, "Migrated verifications: {verifications:?}"); } // ======================= Step 3: ======================= @@ -1466,14 +1493,15 @@ fn migrate_pgp_contacts( // In the process, track the set of contacts which remained no any chat at all // in a `BTreeSet`, which initially contains all contact ids let mut orphaned_contacts: BTreeSet = transaction - .prepare("SELECT id FROM contacts")? + .prepare("SELECT id FROM contacts WHERE id>9")? .query_map((), |row| row.get::(0))? .collect::, rusqlite::Error>>()?; { let mut stmt = transaction.prepare( "SELECT c.id, c.type, c.grpid, c.protected - FROM chats c", + FROM chats c + WHERE id>9", )?; let mut load_chat_contacts_stmt = transaction.prepare("SELECT contact_id FROM chats_contacts WHERE chat_id=?")?; @@ -1493,6 +1521,7 @@ fn migrate_pgp_contacts( .collect::, rusqlite::Error>>()?; let mut keep_email_contacts = || { + info!(context, "Chat {chat_id} will be an unencrypted chat where contacts are identified by email address."); for m in &old_members { orphaned_contacts.remove(m); } @@ -1507,7 +1536,8 @@ fn migrate_pgp_contacts( // and the effect will be the same. 100 => { let Some(old_member) = old_members.first() else { - warn!(context, "1:1 chat doesn't contain contact"); + warn!(context, "1:1 chat {chat_id} doesn't contain contact"); + debug_assert!(false, "1:1 chat {chat_id} doesn't contain contact"); continue; }; let Some(&new_contact) = autocrypt_pgp_contacts.get(old_member) else { @@ -1571,6 +1601,16 @@ fn migrate_pgp_contacts( continue; } + let human_readable_transitions = old_and_new_members + .iter() + .map(|(old, new)| format!("{old}->{}", new.unwrap_or_default())) + .collect::>() + .join(" "); + info!( + context, + "Migrating chat {chat_id} to PGP contacts: {human_readable_transitions}" + ); + for (old_member, new_member) in old_and_new_members { if let Some(new_member) = new_member { orphaned_contacts.remove(&new_member); @@ -1589,7 +1629,10 @@ fn migrate_pgp_contacts( } // ======================= Step 4: ======================= - // Mark all contacts which remained in no chat at all as hidden + info!( + context, + "Marking contacts which remained in no chat at all as hidden: {orphaned_contacts:?}" + ); let mut mark_as_hidden_stmt = transaction.prepare("UPDATE contacts SET origin=? WHERE id=?")?; for contact in orphaned_contacts { mark_as_hidden_stmt.execute((0x8, contact))?; @@ -1635,6 +1678,14 @@ impl Sql { migration: impl Send + FnOnce(&mut rusqlite::Transaction) -> Result<()>, version: i32, ) -> Result<()> { + #[cfg(test)] + if STOP_MIGRATIONS_AT.try_with(|stop_migrations_at| version > *stop_migrations_at) + == Ok(true) + { + println!("Not running migration {version}, because STOP_MIGRATIONS_AT is set"); + return Ok(()); + } + self.transaction(move |transaction| { let curr_version: String = transaction.query_row( "SELECT IFNULL(value, ?) FROM config WHERE keyname=?;", @@ -1658,28 +1709,4 @@ impl Sql { } #[cfg(test)] -mod tests { - use super::*; - use crate::config::Config; - use crate::test_utils::TestContext; - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_clear_config_cache() -> anyhow::Result<()> { - // Some migrations change the `config` table in SQL. - // This test checks that the config cache is invalidated in `execute_migration()`. - - let t = TestContext::new().await; - assert_eq!(t.get_config_bool(Config::IsChatmail).await?, false); - - t.sql - .execute_migration( - "INSERT INTO config (keyname, value) VALUES ('is_chatmail', '1')", - 1000, - ) - .await?; - assert_eq!(t.get_config_bool(Config::IsChatmail).await?, true); - assert_eq!(t.sql.get_raw_config_int(VERSION_CFG).await?.unwrap(), 1000); - - Ok(()) - } -} +mod migrations_tests; diff --git a/src/sql/migrations/migrations_tests.rs b/src/sql/migrations/migrations_tests.rs new file mode 100644 index 0000000000..3ac9f42f52 --- /dev/null +++ b/src/sql/migrations/migrations_tests.rs @@ -0,0 +1,67 @@ +use super::*; +use crate::chat; +use crate::chat::ChatId; +use crate::config::Config; +use crate::contact::Contact; +use crate::contact::Origin; +use crate::test_utils::bob_keypair; +use crate::test_utils::TestContext; +use crate::tools; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_clear_config_cache() -> anyhow::Result<()> { + // Some migrations change the `config` table in SQL. + // This test checks that the config cache is invalidated in `execute_migration()`. + + let t = TestContext::new().await; + assert_eq!(t.get_config_bool(Config::IsChatmail).await?, false); + + t.sql + .execute_migration( + "INSERT INTO config (keyname, value) VALUES ('is_chatmail', '1')", + 1000, + ) + .await?; + assert_eq!(t.get_config_bool(Config::IsChatmail).await?, true); + assert_eq!(t.sql.get_raw_config_int(VERSION_CFG).await?.unwrap(), 1000); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_pgp_contacts_migration() -> Result<()> { + let t = STOP_MIGRATIONS_AT + .scope(131, async move { TestContext::new_alice().await }) + .await; + + t.sql.call_write(|conn| Ok(conn.execute_batch(r#" + INSERT INTO acpeerstates VALUES(1,'bob@example.net',0,0,NULL,1,1745589039,X'c6c04d045e30c757010800cec0b4bfc4277c88a0d652cc937d5cd66f2f9918a3e96a63d3bdd8f41858277f4075101680e7ffcf8c0cdb2b988a8a8e903449996a0cc93e45cf07225c0084549b44f5eada83b42bf19be1fddd8117a478bf5d639e270f64a210134aa52db113b4a4525e0ef3e2313990ac498762858349005f0aba3065dbe730095b27d26360e9e070c793c5cd23c663ece6cd7bc850bed4e5aee1fc160b250cdf0cb527374a4dc0d6af2ad292f9a015d52a27ba490e4d47153b7ec7db6f4252b7ba7f415e2470bf4bb4cc34ae23c7831ff7512c0e142fd3eaeaf9899816a67b504fb04d4f03b573793489476a28257313ea8d80987f0f3d47d192fdce896ba1ecb339152a470011010001cd113c626f62406578616d706c652e6e65743ec2c08b0410010800350219010502680b932f021b03040b090807061508090a0b02031602010127162104cccb5aa9f6e1141c943165f1db18b18cbcf70487000a0910db18b18cbcf70487069707ff53fae1e7d40cf1b2b0ea22c1cadd735b16fbdc4c0571fc88b9c489bb2023fce8e197880c4d579d67fa75124ae696fecc17cd5815362e00601e9240d10e0a46bfc0567b88312a41e56bedb045482de61279eb7d10cf15b23e56dc254084401eeaac0780f7ca912f6f9e3d4e4b3f82b1a0fc3ee6600e5367549dbc83242743dee435287c1ba1db604f4d7416780a5d43fe8047338866715a9081285797b96cb9340822d04331121646188e3c9e9bf209611fe9f72bf5df3f0cfdf46d698566ae5ef75e8fa05f5d760e22e592c61e2a48dffeff8cec2f425a5c04951df78f68362f475ba9a8f15e4f588d85f8738815d92d8ccd876833c1683927dd28f5ede9da8ecec04d045e30c757010800a207812db22369e2482375b6a71b2ef9212eb1090957291b1980edab25d5f970598ac638184d244dac0ae66a9287eac3aaab82c438185814539c667010aa219e3d8d1bbe698dfc953e160c51d26defe61ad68885bd9960aeb3a3d5bb637afab9df216d42894c37e5f6a12f2695ff634b32323c2783c499353758316800138370720320754ddd300dd14fa78f278bcab37f219979889cbc9971ef862739a8dada59c8ff2f88f4bb269aa88e808f0771b987d68779a929d58e17290684c4035e582c8124484dc2d344395129434b711583f20ebb71579cb97bbf4850fe35f2bfcf1ec9c7e949f15c6cc1e8b7d56d2784c83c8a125fb0d0fae53649724a899364550011010001c2c0760418010800200502680b932f021b0c162104cccb5aa9f6e1141c943165f1db18b18cbcf70487000a0910db18b18cbcf704873c3807fd1e3e54a16fc879fc006af060de9216a761188b73fcaa617383feb632b80bcbbf362ea4381bd15e58cffa5ec03da0cd50e4adf37be5c81a66d6a22b9835cbb9c219ecd7426547e6a8ec35839d76795aa448a544bc4a5ecfea0284c1ee576a3dc9fdd41beb54f3f60283451b1d292bddda076e1c02b82d957708dcea5f6fb4faf72f69bdff01ed89468e9870e1a081dac09ccc0b9590ac12e7b85008838e8f9aafcfb2bdcc63085a70819c4f6b8b77cff5716af43c834d114a22745eea504b90c431abadb06ba979021726de29fa09523254ff88d3a9a94ba22c46ba5eb4919ca3c8d1f58b1349c5dd1747afb88067dd2ee258b07b8eb0e09235da2469fcc08c79',NULL,'CCCB5AA9F6E1141C943165F1DB18B18CBCF70487',NULL,NULL,'',NULL,NULL,'',NULL); + INSERT INTO contacts VALUES(10,'','bob@example.net',16384,0,0,'','',1745589041,'',0); + INSERT INTO chats VALUES(10,100,'bob@example.net',0,'',0,'','',0,0,0,0,0,1745589039,0,NULL,0); + INSERT INTO chats_contacts VALUES(10,10,0,0);"#, + )?)).await?; + t.sql.run_migrations(&t).await?; + + //std::thread::sleep(std::time::Duration::from_secs(1000)); + let email_bob_id = Contact::lookup_id_by_addr(&t, "bob@example.net", Origin::Hidden) + .await? + .unwrap(); + let email_bob = Contact::get_by_id(&t, email_bob_id).await?; + assert_eq!(email_bob.origin, Origin::Hidden); // Email bob is in no chats, so, contact is hidden + assert_eq!(email_bob.e2ee_avail(&t).await?, false); + assert_eq!(email_bob.fingerprint(), None); + assert_eq!(email_bob.get_verifier_id(&t).await?, None); + + let bob_chat_contacts = chat::get_chat_contacts(&t, ChatId::new(10)).await?; + let pgp_bob_id = tools::single_value(bob_chat_contacts).unwrap(); + let pgp_bob = Contact::get_by_id(&t, pgp_bob_id).await?; + assert_eq!(pgp_bob.origin, Origin::OutgoingTo); + assert_eq!(pgp_bob.e2ee_avail(&t).await?, true); + assert_eq!( + pgp_bob.fingerprint().unwrap(), + pgp_bob.public_key(&t).await?.unwrap().dc_fingerprint() + ); + assert_eq!(pgp_bob.get_verifier_id(&t).await?, None); + + Ok(()) +} From 7e8a003af4a3f8db5382f04ad5256c6abcc4a31f Mon Sep 17 00:00:00 2001 From: Hocuri Date: Fri, 25 Apr 2025 19:21:29 +0200 Subject: [PATCH 197/381] formatting --- src/sql/migrations.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 9dcfc611ee..9cff013abc 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1328,13 +1328,13 @@ fn migrate_pgp_contacts( let mut load_contacts_stmt = transaction.prepare( "SELECT c.id, c.name, c.addr, c.origin, c.blocked, c.last_seen, - c.authname, c.param, c.status, c.is_bot, c.selfavatar_sent, - IFNULL(p.public_key, p.gossip_key), - p.verified_key, p.verifier, - p.secondary_verified_key, p.secondary_verifier, p.prefer_encrypted - FROM contacts c - INNER JOIN acpeerstates p ON c.addr=p.addr - WHERE c.id > 9", + c.authname, c.param, c.status, c.is_bot, c.selfavatar_sent, + IFNULL(p.public_key, p.gossip_key), + p.verified_key, p.verifier, + p.secondary_verified_key, p.secondary_verifier, p.prefer_encrypted + FROM contacts c + INNER JOIN acpeerstates p ON c.addr=p.addr + WHERE c.id > 9", )?; let all_email_contacts = load_contacts_stmt.query_map((), |row| { @@ -1387,8 +1387,8 @@ fn migrate_pgp_contacts( let mut insert_contact_stmt = transaction.prepare( "INSERT INTO contacts (name, addr, origin, blocked, last_seen, - authname, param, status, is_bot, selfavatar_sent, fingerprint) - VALUES(?,?,?,?,?,?,?,?,?,?,?)", + authname, param, status, is_bot, selfavatar_sent, fingerprint) + VALUES(?,?,?,?,?,?,?,?,?,?,?)", )?; let mut fingerprint_to_id_stmt = transaction.prepare("SELECT id FROM contacts WHERE fingerprint=?")?; From c169712e029c34c1f2bf7622b280052df25ef17c Mon Sep 17 00:00:00 2001 From: Hocuri Date: Fri, 25 Apr 2025 22:39:05 +0200 Subject: [PATCH 198/381] Add another test & fix problems I found --- src/sql/migrations.rs | 117 ++++++++++++++----------- src/sql/migrations/migrations_tests.rs | 56 +++++++++++- 2 files changed, 117 insertions(+), 56 deletions(-) diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 9cff013abc..4675d1a9d9 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -5,6 +5,7 @@ use std::collections::BTreeSet; use std::time::Instant; use anyhow::{ensure, Context as _, Result}; +use deltachat_contact_tools::addr_cmp; use deltachat_contact_tools::EmailAddress; use pgp::SignedPublicKey; use rusqlite::OptionalExtension; @@ -12,6 +13,7 @@ use rusqlite::OptionalExtension; use crate::config::Config; use crate::configure::EnteredLoginParam; use crate::constants::ShowEmails; +use crate::contact::ContactId; use crate::context::Context; use crate::imap; use crate::key::DcKey; @@ -1265,6 +1267,12 @@ fn migrate_pgp_contacts( transaction: &mut rusqlite::Transaction<'_>, ) -> std::result::Result<(), anyhow::Error> { info!(context, "Starting PGP contact transition."); + let self_addr: String = transaction.query_row( + "SELECT value FROM config WHERE keyname='configured_addr'", + (), + |row| row.get(0), + )?; + // =============================== Step 1: =============================== // Alter tables transaction.execute_batch( @@ -1337,53 +1345,55 @@ fn migrate_pgp_contacts( WHERE c.id > 9", )?; - let all_email_contacts = load_contacts_stmt.query_map((), |row| { - let id: i64 = row.get(0)?; - let name: String = row.get(1)?; - let addr: String = row.get(2)?; - let origin: i64 = row.get(3)?; - let blocked: Option = row.get(4)?; - let last_seen: i64 = row.get(5)?; - let authname: String = row.get(6)?; - let param: String = row.get(7)?; - let status: Option = row.get(8)?; - let is_bot: bool = row.get(9)?; - let selfavatar_sent: i64 = row.get(10)?; - let autocrypt_key = row - .get(11) - .ok() - .and_then(|blob: Vec| SignedPublicKey::from_slice(&blob).ok()); - let verified_key = row - .get(12) - .ok() - .and_then(|blob: Vec| SignedPublicKey::from_slice(&blob).ok()); - let verifier: String = row.get(13)?; - let secondary_verified_key = row - .get(12) - .ok() - .and_then(|blob: Vec| SignedPublicKey::from_slice(&blob).ok()); - let secondary_verifier: String = row.get(15)?; - let prefer_encrypt: u8 = row.get(16)?; - Ok(( - id, - name, - addr, - origin, - blocked, - last_seen, - authname, - param, - status, - is_bot, - selfavatar_sent, - autocrypt_key, - verified_key, - verifier, - secondary_verified_key, - secondary_verifier, - prefer_encrypt, - )) - })?; + let all_email_contacts: rusqlite::Result> = load_contacts_stmt + .query_map((), |row| { + let id: i64 = row.get(0)?; + let name: String = row.get(1)?; + let addr: String = row.get(2)?; + let origin: i64 = row.get(3)?; + let blocked: Option = row.get(4)?; + let last_seen: i64 = row.get(5)?; + let authname: String = row.get(6)?; + let param: String = row.get(7)?; + let status: Option = row.get(8)?; + let is_bot: bool = row.get(9)?; + let selfavatar_sent: i64 = row.get(10)?; + let autocrypt_key = row + .get(11) + .ok() + .and_then(|blob: Vec| SignedPublicKey::from_slice(&blob).ok()); + let verified_key = row + .get(12) + .ok() + .and_then(|blob: Vec| SignedPublicKey::from_slice(&blob).ok()); + let verifier: String = row.get(13)?; + let secondary_verified_key = row + .get(12) + .ok() + .and_then(|blob: Vec| SignedPublicKey::from_slice(&blob).ok()); + let secondary_verifier: String = row.get(15)?; + let prefer_encrypt: u8 = row.get(16)?; + Ok(( + id, + name, + addr, + origin, + blocked, + last_seen, + authname, + param, + status, + is_bot, + selfavatar_sent, + autocrypt_key, + verified_key, + verifier, + secondary_verified_key, + secondary_verifier, + prefer_encrypt, + )) + })? + .collect(); let mut insert_contact_stmt = transaction.prepare( "INSERT INTO contacts (name, addr, origin, blocked, last_seen, @@ -1395,7 +1405,7 @@ fn migrate_pgp_contacts( let mut original_contact_id_from_addr_stmt = transaction.prepare("SELECT id FROM contacts WHERE addr=? AND fingerprint=''")?; - for row in all_email_contacts { + for row in all_email_contacts? { let ( original_id, name, @@ -1414,7 +1424,7 @@ fn migrate_pgp_contacts( secondary_verified_key, secondary_verifier, prefer_encrypt, - ) = row?; + ) = row; let mut insert_contact = |key: SignedPublicKey| -> Result { let fingerprint = key.dc_fingerprint().hex(); let existing_contact_id: Option = fingerprint_to_id_stmt @@ -1443,7 +1453,10 @@ fn migrate_pgp_contacts( ); Ok(id) }; - let mut original_contact_id_from_addr = |addr: &str| { + let mut original_contact_id_from_addr = |addr: &str| -> rusqlite::Result { + if addr_cmp(addr, &self_addr) { + return Ok(1); // ContactId::SELF + } original_contact_id_from_addr_stmt.query_row((addr,), |row| row.get(0)) }; @@ -1503,8 +1516,8 @@ fn migrate_pgp_contacts( FROM chats c WHERE id>9", )?; - let mut load_chat_contacts_stmt = - transaction.prepare("SELECT contact_id FROM chats_contacts WHERE chat_id=?")?; + let mut load_chat_contacts_stmt = transaction + .prepare("SELECT contact_id FROM chats_contacts WHERE chat_id=? AND contact_id>9")?; let all_chats = stmt.query_map((), |row| { let id: u32 = row.get(0)?; let typ: u32 = row.get(1)?; diff --git a/src/sql/migrations/migrations_tests.rs b/src/sql/migrations/migrations_tests.rs index 3ac9f42f52..e64be7d019 100644 --- a/src/sql/migrations/migrations_tests.rs +++ b/src/sql/migrations/migrations_tests.rs @@ -35,10 +35,10 @@ async fn test_pgp_contacts_migration() -> Result<()> { .await; t.sql.call_write(|conn| Ok(conn.execute_batch(r#" - INSERT INTO acpeerstates VALUES(1,'bob@example.net',0,0,NULL,1,1745589039,X'c6c04d045e30c757010800cec0b4bfc4277c88a0d652cc937d5cd66f2f9918a3e96a63d3bdd8f41858277f4075101680e7ffcf8c0cdb2b988a8a8e903449996a0cc93e45cf07225c0084549b44f5eada83b42bf19be1fddd8117a478bf5d639e270f64a210134aa52db113b4a4525e0ef3e2313990ac498762858349005f0aba3065dbe730095b27d26360e9e070c793c5cd23c663ece6cd7bc850bed4e5aee1fc160b250cdf0cb527374a4dc0d6af2ad292f9a015d52a27ba490e4d47153b7ec7db6f4252b7ba7f415e2470bf4bb4cc34ae23c7831ff7512c0e142fd3eaeaf9899816a67b504fb04d4f03b573793489476a28257313ea8d80987f0f3d47d192fdce896ba1ecb339152a470011010001cd113c626f62406578616d706c652e6e65743ec2c08b0410010800350219010502680b932f021b03040b090807061508090a0b02031602010127162104cccb5aa9f6e1141c943165f1db18b18cbcf70487000a0910db18b18cbcf70487069707ff53fae1e7d40cf1b2b0ea22c1cadd735b16fbdc4c0571fc88b9c489bb2023fce8e197880c4d579d67fa75124ae696fecc17cd5815362e00601e9240d10e0a46bfc0567b88312a41e56bedb045482de61279eb7d10cf15b23e56dc254084401eeaac0780f7ca912f6f9e3d4e4b3f82b1a0fc3ee6600e5367549dbc83242743dee435287c1ba1db604f4d7416780a5d43fe8047338866715a9081285797b96cb9340822d04331121646188e3c9e9bf209611fe9f72bf5df3f0cfdf46d698566ae5ef75e8fa05f5d760e22e592c61e2a48dffeff8cec2f425a5c04951df78f68362f475ba9a8f15e4f588d85f8738815d92d8ccd876833c1683927dd28f5ede9da8ecec04d045e30c757010800a207812db22369e2482375b6a71b2ef9212eb1090957291b1980edab25d5f970598ac638184d244dac0ae66a9287eac3aaab82c438185814539c667010aa219e3d8d1bbe698dfc953e160c51d26defe61ad68885bd9960aeb3a3d5bb637afab9df216d42894c37e5f6a12f2695ff634b32323c2783c499353758316800138370720320754ddd300dd14fa78f278bcab37f219979889cbc9971ef862739a8dada59c8ff2f88f4bb269aa88e808f0771b987d68779a929d58e17290684c4035e582c8124484dc2d344395129434b711583f20ebb71579cb97bbf4850fe35f2bfcf1ec9c7e949f15c6cc1e8b7d56d2784c83c8a125fb0d0fae53649724a899364550011010001c2c0760418010800200502680b932f021b0c162104cccb5aa9f6e1141c943165f1db18b18cbcf70487000a0910db18b18cbcf704873c3807fd1e3e54a16fc879fc006af060de9216a761188b73fcaa617383feb632b80bcbbf362ea4381bd15e58cffa5ec03da0cd50e4adf37be5c81a66d6a22b9835cbb9c219ecd7426547e6a8ec35839d76795aa448a544bc4a5ecfea0284c1ee576a3dc9fdd41beb54f3f60283451b1d292bddda076e1c02b82d957708dcea5f6fb4faf72f69bdff01ed89468e9870e1a081dac09ccc0b9590ac12e7b85008838e8f9aafcfb2bdcc63085a70819c4f6b8b77cff5716af43c834d114a22745eea504b90c431abadb06ba979021726de29fa09523254ff88d3a9a94ba22c46ba5eb4919ca3c8d1f58b1349c5dd1747afb88067dd2ee258b07b8eb0e09235da2469fcc08c79',NULL,'CCCB5AA9F6E1141C943165F1DB18B18CBCF70487',NULL,NULL,'',NULL,NULL,'',NULL); - INSERT INTO contacts VALUES(10,'','bob@example.net',16384,0,0,'','',1745589041,'',0); - INSERT INTO chats VALUES(10,100,'bob@example.net',0,'',0,'','',0,0,0,0,0,1745589039,0,NULL,0); - INSERT INTO chats_contacts VALUES(10,10,0,0);"#, + INSERT INTO acpeerstates VALUES(1,'bob@example.net',0,0,NULL,1,1745589039,X'c6c04d045e30c757010800cec0b4bfc4277c88a0d652cc937d5cd66f2f9918a3e96a63d3bdd8f41858277f4075101680e7ffcf8c0cdb2b988a8a8e903449996a0cc93e45cf07225c0084549b44f5eada83b42bf19be1fddd8117a478bf5d639e270f64a210134aa52db113b4a4525e0ef3e2313990ac498762858349005f0aba3065dbe730095b27d26360e9e070c793c5cd23c663ece6cd7bc850bed4e5aee1fc160b250cdf0cb527374a4dc0d6af2ad292f9a015d52a27ba490e4d47153b7ec7db6f4252b7ba7f415e2470bf4bb4cc34ae23c7831ff7512c0e142fd3eaeaf9899816a67b504fb04d4f03b573793489476a28257313ea8d80987f0f3d47d192fdce896ba1ecb339152a470011010001cd113c626f62406578616d706c652e6e65743ec2c08b0410010800350219010502680b932f021b03040b090807061508090a0b02031602010127162104cccb5aa9f6e1141c943165f1db18b18cbcf70487000a0910db18b18cbcf70487069707ff53fae1e7d40cf1b2b0ea22c1cadd735b16fbdc4c0571fc88b9c489bb2023fce8e197880c4d579d67fa75124ae696fecc17cd5815362e00601e9240d10e0a46bfc0567b88312a41e56bedb045482de61279eb7d10cf15b23e56dc254084401eeaac0780f7ca912f6f9e3d4e4b3f82b1a0fc3ee6600e5367549dbc83242743dee435287c1ba1db604f4d7416780a5d43fe8047338866715a9081285797b96cb9340822d04331121646188e3c9e9bf209611fe9f72bf5df3f0cfdf46d698566ae5ef75e8fa05f5d760e22e592c61e2a48dffeff8cec2f425a5c04951df78f68362f475ba9a8f15e4f588d85f8738815d92d8ccd876833c1683927dd28f5ede9da8ecec04d045e30c757010800a207812db22369e2482375b6a71b2ef9212eb1090957291b1980edab25d5f970598ac638184d244dac0ae66a9287eac3aaab82c438185814539c667010aa219e3d8d1bbe698dfc953e160c51d26defe61ad68885bd9960aeb3a3d5bb637afab9df216d42894c37e5f6a12f2695ff634b32323c2783c499353758316800138370720320754ddd300dd14fa78f278bcab37f219979889cbc9971ef862739a8dada59c8ff2f88f4bb269aa88e808f0771b987d68779a929d58e17290684c4035e582c8124484dc2d344395129434b711583f20ebb71579cb97bbf4850fe35f2bfcf1ec9c7e949f15c6cc1e8b7d56d2784c83c8a125fb0d0fae53649724a899364550011010001c2c0760418010800200502680b932f021b0c162104cccb5aa9f6e1141c943165f1db18b18cbcf70487000a0910db18b18cbcf704873c3807fd1e3e54a16fc879fc006af060de9216a761188b73fcaa617383feb632b80bcbbf362ea4381bd15e58cffa5ec03da0cd50e4adf37be5c81a66d6a22b9835cbb9c219ecd7426547e6a8ec35839d76795aa448a544bc4a5ecfea0284c1ee576a3dc9fdd41beb54f3f60283451b1d292bddda076e1c02b82d957708dcea5f6fb4faf72f69bdff01ed89468e9870e1a081dac09ccc0b9590ac12e7b85008838e8f9aafcfb2bdcc63085a70819c4f6b8b77cff5716af43c834d114a22745eea504b90c431abadb06ba979021726de29fa09523254ff88d3a9a94ba22c46ba5eb4919ca3c8d1f58b1349c5dd1747afb88067dd2ee258b07b8eb0e09235da2469fcc08c79',NULL,'CCCB5AA9F6E1141C943165F1DB18B18CBCF70487',NULL,NULL,'',NULL,NULL,'',NULL); + INSERT INTO contacts VALUES(10,'','bob@example.net',16384,0,0,'','',1745589041,'',0); + INSERT INTO chats VALUES(10,100,'bob@example.net',0,'',0,'','',0,0,0,0,0,1745589039,0,NULL,0); + INSERT INTO chats_contacts VALUES(10,10,0,0);"#, )?)).await?; t.sql.run_migrations(&t).await?; @@ -65,3 +65,51 @@ async fn test_pgp_contacts_migration() -> Result<()> { Ok(()) } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_pgp_contacts_migration_verified() -> Result<()> { + let t = STOP_MIGRATIONS_AT + .scope(131, async move { TestContext::new_alice().await }) + .await; + + t.sql.call_write(|conn| Ok(conn.execute_batch(r#" + INSERT INTO acpeerstates VALUES(1,'bob@example.net',0,0,X'c6c04d045e30c757010800cec0b4bfc4277c88a0d652cc937d5cd66f2f9918a3e96a63d3bdd8f41858277f4075101680e7ffcf8c0cdb2b988a8a8e903449996a0cc93e45cf07225c0084549b44f5eada83b42bf19be1fddd8117a478bf5d639e270f64a210134aa52db113b4a4525e0ef3e2313990ac498762858349005f0aba3065dbe730095b27d26360e9e070c793c5cd23c663ece6cd7bc850bed4e5aee1fc160b250cdf0cb527374a4dc0d6af2ad292f9a015d52a27ba490e4d47153b7ec7db6f4252b7ba7f415e2470bf4bb4cc34ae23c7831ff7512c0e142fd3eaeaf9899816a67b504fb04d4f03b573793489476a28257313ea8d80987f0f3d47d192fdce896ba1ecb339152a470011010001cd113c626f62406578616d706c652e6e65743ec2c08b0410010800350219010502680be34b021b03040b090807061508090a0b02031602010127162104cccb5aa9f6e1141c943165f1db18b18cbcf70487000a0910db18b18cbcf70487921c07ff4f16327797bc5836071b5fcf2ae269c609b697c38b579f2449d0ff07f3e04db07822bfa83a6ca85308d99328c765f9b37b883a3526d38c3c005810ee6d9064acae1c68784781b9688be535a03ed5902d9ab5c9e5d28fb04aa621cb294445b9eab122d86afc0e2a4fd9a6a9af82f50b49295a9852f35c9ed8d816218ba98bc047cfe5fb9432e45ea63140bd16263728b1d1dd18d143b677e1ddd9cb5e939dd51cd7f2c2037cc89b5cee26917ea949e31c808996a5b7efc73636511173f59e2ab025902d86085110ac22988e86e663f19514c559a3b5a52838d1d1fa263f065fddf5fc8c8a1b4dac51aece76d536a3426f133be204dcb03c4a84242137373e39e7cec04d045e30c757010800a207812db22369e2482375b6a71b2ef9212eb1090957291b1980edab25d5f970598ac638184d244dac0ae66a9287eac3aaab82c438185814539c667010aa219e3d8d1bbe698dfc953e160c51d26defe61ad68885bd9960aeb3a3d5bb637afab9df216d42894c37e5f6a12f2695ff634b32323c2783c499353758316800138370720320754ddd300dd14fa78f278bcab37f219979889cbc9971ef862739a8dada59c8ff2f88f4bb269aa88e808f0771b987d68779a929d58e17290684c4035e582c8124484dc2d344395129434b711583f20ebb71579cb97bbf4850fe35f2bfcf1ec9c7e949f15c6cc1e8b7d56d2784c83c8a125fb0d0fae53649724a899364550011010001c2c0760418010800200502680be34b021b0c162104cccb5aa9f6e1141c943165f1db18b18cbcf70487000a0910db18b18cbcf7048798cf07fc09e848aa6595d435805efbbbd0b05bec2fffa88b3b1d6e0a3ba80e300bdd83aa5f03bfcc9361b2a90e9cf8980c775d707467c638a13d65a01eda4b57d3560fada0c675399c263e668a02b84733b2c8e71d7fb9f15cb076f933571d32fc3377bb59ff64da6808eeb96210776126504ae9d6916124d3c679ba810a6c92dfe7d58eba7df22e9f07241d343d3e1792fe48d36fd6ec7d1ed291eae5d5d688872f5c723d5a12c424ff32c25d1348d2b683c5cf9128efb957b0026e607d593528a01dea6458c4709779b8f99bd689ef1bfde7146461317ee2793a130663388977488a9fd1a652377445571b1c913ee14fe0b22d451943b4fe1d0578b71201f1ee106f4c',1,1745609547,X'c6c04d045e30c757010800cec0b4bfc4277c88a0d652cc937d5cd66f2f9918a3e96a63d3bdd8f41858277f4075101680e7ffcf8c0cdb2b988a8a8e903449996a0cc93e45cf07225c0084549b44f5eada83b42bf19be1fddd8117a478bf5d639e270f64a210134aa52db113b4a4525e0ef3e2313990ac498762858349005f0aba3065dbe730095b27d26360e9e070c793c5cd23c663ece6cd7bc850bed4e5aee1fc160b250cdf0cb527374a4dc0d6af2ad292f9a015d52a27ba490e4d47153b7ec7db6f4252b7ba7f415e2470bf4bb4cc34ae23c7831ff7512c0e142fd3eaeaf9899816a67b504fb04d4f03b573793489476a28257313ea8d80987f0f3d47d192fdce896ba1ecb339152a470011010001cd113c626f62406578616d706c652e6e65743ec2c08b0410010800350219010502680be34b021b03040b090807061508090a0b02031602010127162104cccb5aa9f6e1141c943165f1db18b18cbcf70487000a0910db18b18cbcf70487921c07ff4f16327797bc5836071b5fcf2ae269c609b697c38b579f2449d0ff07f3e04db07822bfa83a6ca85308d99328c765f9b37b883a3526d38c3c005810ee6d9064acae1c68784781b9688be535a03ed5902d9ab5c9e5d28fb04aa621cb294445b9eab122d86afc0e2a4fd9a6a9af82f50b49295a9852f35c9ed8d816218ba98bc047cfe5fb9432e45ea63140bd16263728b1d1dd18d143b677e1ddd9cb5e939dd51cd7f2c2037cc89b5cee26917ea949e31c808996a5b7efc73636511173f59e2ab025902d86085110ac22988e86e663f19514c559a3b5a52838d1d1fa263f065fddf5fc8c8a1b4dac51aece76d536a3426f133be204dcb03c4a84242137373e39e7cec04d045e30c757010800a207812db22369e2482375b6a71b2ef9212eb1090957291b1980edab25d5f970598ac638184d244dac0ae66a9287eac3aaab82c438185814539c667010aa219e3d8d1bbe698dfc953e160c51d26defe61ad68885bd9960aeb3a3d5bb637afab9df216d42894c37e5f6a12f2695ff634b32323c2783c499353758316800138370720320754ddd300dd14fa78f278bcab37f219979889cbc9971ef862739a8dada59c8ff2f88f4bb269aa88e808f0771b987d68779a929d58e17290684c4035e582c8124484dc2d344395129434b711583f20ebb71579cb97bbf4850fe35f2bfcf1ec9c7e949f15c6cc1e8b7d56d2784c83c8a125fb0d0fae53649724a899364550011010001c2c0760418010800200502680be34b021b0c162104cccb5aa9f6e1141c943165f1db18b18cbcf70487000a0910db18b18cbcf7048798cf07fc09e848aa6595d435805efbbbd0b05bec2fffa88b3b1d6e0a3ba80e300bdd83aa5f03bfcc9361b2a90e9cf8980c775d707467c638a13d65a01eda4b57d3560fada0c675399c263e668a02b84733b2c8e71d7fb9f15cb076f933571d32fc3377bb59ff64da6808eeb96210776126504ae9d6916124d3c679ba810a6c92dfe7d58eba7df22e9f07241d343d3e1792fe48d36fd6ec7d1ed291eae5d5d688872f5c723d5a12c424ff32c25d1348d2b683c5cf9128efb957b0026e607d593528a01dea6458c4709779b8f99bd689ef1bfde7146461317ee2793a130663388977488a9fd1a652377445571b1c913ee14fe0b22d451943b4fe1d0578b71201f1ee106f4c','CCCB5AA9F6E1141C943165F1DB18B18CBCF70487','CCCB5AA9F6E1141C943165F1DB18B18CBCF70487',X'c6c04d045e30c757010800cec0b4bfc4277c88a0d652cc937d5cd66f2f9918a3e96a63d3bdd8f41858277f4075101680e7ffcf8c0cdb2b988a8a8e903449996a0cc93e45cf07225c0084549b44f5eada83b42bf19be1fddd8117a478bf5d639e270f64a210134aa52db113b4a4525e0ef3e2313990ac498762858349005f0aba3065dbe730095b27d26360e9e070c793c5cd23c663ece6cd7bc850bed4e5aee1fc160b250cdf0cb527374a4dc0d6af2ad292f9a015d52a27ba490e4d47153b7ec7db6f4252b7ba7f415e2470bf4bb4cc34ae23c7831ff7512c0e142fd3eaeaf9899816a67b504fb04d4f03b573793489476a28257313ea8d80987f0f3d47d192fdce896ba1ecb339152a470011010001cd113c626f62406578616d706c652e6e65743ec2c08b0410010800350219010502680be34b021b03040b090807061508090a0b02031602010127162104cccb5aa9f6e1141c943165f1db18b18cbcf70487000a0910db18b18cbcf70487921c07ff4f16327797bc5836071b5fcf2ae269c609b697c38b579f2449d0ff07f3e04db07822bfa83a6ca85308d99328c765f9b37b883a3526d38c3c005810ee6d9064acae1c68784781b9688be535a03ed5902d9ab5c9e5d28fb04aa621cb294445b9eab122d86afc0e2a4fd9a6a9af82f50b49295a9852f35c9ed8d816218ba98bc047cfe5fb9432e45ea63140bd16263728b1d1dd18d143b677e1ddd9cb5e939dd51cd7f2c2037cc89b5cee26917ea949e31c808996a5b7efc73636511173f59e2ab025902d86085110ac22988e86e663f19514c559a3b5a52838d1d1fa263f065fddf5fc8c8a1b4dac51aece76d536a3426f133be204dcb03c4a84242137373e39e7cec04d045e30c757010800a207812db22369e2482375b6a71b2ef9212eb1090957291b1980edab25d5f970598ac638184d244dac0ae66a9287eac3aaab82c438185814539c667010aa219e3d8d1bbe698dfc953e160c51d26defe61ad68885bd9960aeb3a3d5bb637afab9df216d42894c37e5f6a12f2695ff634b32323c2783c499353758316800138370720320754ddd300dd14fa78f278bcab37f219979889cbc9971ef862739a8dada59c8ff2f88f4bb269aa88e808f0771b987d68779a929d58e17290684c4035e582c8124484dc2d344395129434b711583f20ebb71579cb97bbf4850fe35f2bfcf1ec9c7e949f15c6cc1e8b7d56d2784c83c8a125fb0d0fae53649724a899364550011010001c2c0760418010800200502680be34b021b0c162104cccb5aa9f6e1141c943165f1db18b18cbcf70487000a0910db18b18cbcf7048798cf07fc09e848aa6595d435805efbbbd0b05bec2fffa88b3b1d6e0a3ba80e300bdd83aa5f03bfcc9361b2a90e9cf8980c775d707467c638a13d65a01eda4b57d3560fada0c675399c263e668a02b84733b2c8e71d7fb9f15cb076f933571d32fc3377bb59ff64da6808eeb96210776126504ae9d6916124d3c679ba810a6c92dfe7d58eba7df22e9f07241d343d3e1792fe48d36fd6ec7d1ed291eae5d5d688872f5c723d5a12c424ff32c25d1348d2b683c5cf9128efb957b0026e607d593528a01dea6458c4709779b8f99bd689ef1bfde7146461317ee2793a130663388977488a9fd1a652377445571b1c913ee14fe0b22d451943b4fe1d0578b71201f1ee106f4c','CCCB5AA9F6E1141C943165F1DB18B18CBCF70487','',NULL,NULL,'',1); + INSERT INTO contacts VALUES(10,'','bob@example.net',16384,0,0,'','',1745609549,'',0); + INSERT INTO msgs VALUES(10,'29b4af31-1560-4bc8-9b2b-083f2a3d0432@localhost','',0,10,2,2,1745609547,10,13,1,0,'Messages are guaranteed to be end-to-end encrypted from now on.','','S=11',0,0,0,0,NULL,'',NULL,1,0,'',0,0,0,'',0,NULL,0,NULL,0); + INSERT INTO msgs VALUES(11,'411b3fdd-a20c-48c7-b94d-19c04654a1c5@localhost','',0,10,1,0,1745609548,10,26,1,0,'Hello!','',replace('A=1\nc=1','\n',char(10)),0,0,0,0,X'','','411b3fdd-a20c-48c7-b94d-19c04654a1c5@localhost',1,0,'',0,0,0,'Group',0,NULL,1,NULL,0); + INSERT INTO chats VALUES(10,120,'Group',0,'',0,'-PYdPTYhrEl9L_C6osfpEpQu','g=1745609548',0,0,0,0,0,1745609547,0,NULL,1); + INSERT INTO chats_contacts VALUES(10,1,1745609547,0); + INSERT INTO chats_contacts VALUES(10,10,1745609547,0); + "#, + )?)).await?; + t.sql.run_migrations(&t).await?; + + //std::thread::sleep(std::time::Duration::from_secs(1000)); + let email_bob_id = Contact::lookup_id_by_addr(&t, "bob@example.net", Origin::Hidden) + .await? + .unwrap(); + let email_bob = Contact::get_by_id(&t, email_bob_id).await?; + dbg!(&email_bob); + assert_eq!(email_bob.origin, Origin::Hidden); // Email bob is in no chats, so, contact is hidden + assert_eq!(email_bob.e2ee_avail(&t).await?, false); + assert_eq!(email_bob.fingerprint(), None); + assert_eq!(email_bob.get_verifier_id(&t).await?, None); + + let bob_chat_contacts = chat::get_chat_contacts(&t, ChatId::new(10)).await?; + assert_eq!(bob_chat_contacts.len(), 2); + dbg!(&bob_chat_contacts); + let pgp_bob_id = ContactId::new(11); + assert!(bob_chat_contacts.contains(&pgp_bob_id)); + assert!(bob_chat_contacts.contains(&ContactId::SELF)); + let pgp_bob = Contact::get_by_id(&t, pgp_bob_id).await?; + dbg!(&pgp_bob); + assert_eq!(pgp_bob.origin, Origin::OutgoingTo); + assert_eq!(pgp_bob.e2ee_avail(&t).await?, true); + assert_eq!( + pgp_bob.fingerprint().unwrap(), + pgp_bob.public_key(&t).await?.unwrap().dc_fingerprint() + ); + assert_eq!(pgp_bob.get_verifier_id(&t).await?.unwrap(), ContactId::SELF); + + Ok(()) +} From e94b6358c83a5fc22a3bffff85d32101eca2a03a Mon Sep 17 00:00:00 2001 From: Hocuri Date: Fri, 25 Apr 2025 23:30:52 +0200 Subject: [PATCH 199/381] Migrate verifications, migration should be completely implemented now --- src/sql/migrations.rs | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 4675d1a9d9..d57610c775 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1332,7 +1332,7 @@ fn migrate_pgp_contacts( // It can't map to the verified pgp contact id, because at the time of constructing // this map, not all pgp contacts are in the database. // TODO apply verifications - let mut verifications = BTreeMap::new(); + let mut verifications: BTreeMap = BTreeMap::new(); let mut load_contacts_stmt = transaction.prepare( "SELECT c.id, c.name, c.addr, c.origin, c.blocked, c.last_seen, @@ -1449,11 +1449,11 @@ fn migrate_pgp_contacts( let id = transaction.last_insert_rowid().try_into()?; info!( context, - "Inserted new contact id={id} name={name} addr={addr} fingerprint={fingerprint}" + "Inserted new contact id={id} name='{name}' addr='{addr}' fingerprint={fingerprint}" ); Ok(id) }; - let mut original_contact_id_from_addr = |addr: &str| -> rusqlite::Result { + let mut original_contact_id_from_addr = |addr: &str| -> rusqlite::Result { if addr_cmp(addr, &self_addr) { return Ok(1); // ContactId::SELF } @@ -1478,14 +1478,14 @@ fn migrate_pgp_contacts( }; let new_id = insert_contact(verified_key)?; verified_pgp_contacts.insert(original_id.try_into()?, new_id); - let verifier_id: i64 = original_contact_id_from_addr(&verifier)?; + let verifier_id: u32 = original_contact_id_from_addr(&verifier)?; verifications.insert(new_id, verifier_id); let Some(secondary_verified_key) = secondary_verified_key else { continue; }; let new_id = insert_contact(secondary_verified_key)?; - let verifier_id: i64 = original_contact_id_from_addr(&secondary_verifier)?; + let verifier_id: u32 = original_contact_id_from_addr(&secondary_verifier)?; // Only use secondary verification if there is no primary verification: verifications.entry(new_id).or_insert(verifier_id); } @@ -1498,6 +1498,22 @@ fn migrate_pgp_contacts( context, "Created PGP contacts identified by verified key: {verified_pgp_contacts:?}" ); + + for (&new_contact, &verifier_original_contact) in &verifications { + let verifier = if verifier_original_contact == 1 { + Some(&1) // Verified by ContactId::SELF + } else { + // `verifications` contains the original contact id. + // We need to get the new, verified-pgp-identified contact id. + verified_pgp_contacts.get(&verifier_original_contact) + }; + if let Some(&verifier) = verifier { + transaction.execute( + "UPDATE contacts SET verifier=? WHERE id=?", + (verifier, new_contact), + )?; + } + } info!(context, "Migrated verifications: {verifications:?}"); } @@ -1534,7 +1550,7 @@ fn migrate_pgp_contacts( .collect::, rusqlite::Error>>()?; let mut keep_email_contacts = || { - info!(context, "Chat {chat_id} will be an unencrypted chat where contacts are identified by email address."); + info!(context, "Chat {chat_id} will be an unencrypted chat with contacts identified by email address."); for m in &old_members { orphaned_contacts.remove(m); } From dae62b1dd1795d87b439ef36332aa5193084c1e9 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Fri, 25 Apr 2025 23:31:23 +0200 Subject: [PATCH 200/381] Add two more tests, without finding any bugs --- src/sql/migrations/migrations_tests.rs | 63 +++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/src/sql/migrations/migrations_tests.rs b/src/sql/migrations/migrations_tests.rs index e64be7d019..f33f6d95cf 100644 --- a/src/sql/migrations/migrations_tests.rs +++ b/src/sql/migrations/migrations_tests.rs @@ -29,7 +29,7 @@ async fn test_clear_config_cache() -> anyhow::Result<()> { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_pgp_contacts_migration() -> Result<()> { +async fn test_pgp_contacts_migration_autocrypt() -> Result<()> { let t = STOP_MIGRATIONS_AT .scope(131, async move { TestContext::new_alice().await }) .await; @@ -66,6 +66,67 @@ async fn test_pgp_contacts_migration() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_pgp_contacts_migration_email1() -> Result<()> { + let t = STOP_MIGRATIONS_AT + .scope(131, async move { TestContext::new_alice().await }) + .await; + + t.sql.call_write(|conn| Ok(conn.execute_batch(r#" + INSERT INTO acpeerstates VALUES(1,'bob@example.net',0,0,NULL,1,1745589039,X'c6c04d045e30c757010800cec0b4bfc4277c88a0d652cc937d5cd66f2f9918a3e96a63d3bdd8f41858277f4075101680e7ffcf8c0cdb2b988a8a8e903449996a0cc93e45cf07225c0084549b44f5eada83b42bf19be1fddd8117a478bf5d639e270f64a210134aa52db113b4a4525e0ef3e2313990ac498762858349005f0aba3065dbe730095b27d26360e9e070c793c5cd23c663ece6cd7bc850bed4e5aee1fc160b250cdf0cb527374a4dc0d6af2ad292f9a015d52a27ba490e4d47153b7ec7db6f4252b7ba7f415e2470bf4bb4cc34ae23c7831ff7512c0e142fd3eaeaf9899816a67b504fb04d4f03b573793489476a28257313ea8d80987f0f3d47d192fdce896ba1ecb339152a470011010001cd113c626f62406578616d706c652e6e65743ec2c08b0410010800350219010502680b932f021b03040b090807061508090a0b02031602010127162104cccb5aa9f6e1141c943165f1db18b18cbcf70487000a0910db18b18cbcf70487069707ff53fae1e7d40cf1b2b0ea22c1cadd735b16fbdc4c0571fc88b9c489bb2023fce8e197880c4d579d67fa75124ae696fecc17cd5815362e00601e9240d10e0a46bfc0567b88312a41e56bedb045482de61279eb7d10cf15b23e56dc254084401eeaac0780f7ca912f6f9e3d4e4b3f82b1a0fc3ee6600e5367549dbc83242743dee435287c1ba1db604f4d7416780a5d43fe8047338866715a9081285797b96cb9340822d04331121646188e3c9e9bf209611fe9f72bf5df3f0cfdf46d698566ae5ef75e8fa05f5d760e22e592c61e2a48dffeff8cec2f425a5c04951df78f68362f475ba9a8f15e4f588d85f8738815d92d8ccd876833c1683927dd28f5ede9da8ecec04d045e30c757010800a207812db22369e2482375b6a71b2ef9212eb1090957291b1980edab25d5f970598ac638184d244dac0ae66a9287eac3aaab82c438185814539c667010aa219e3d8d1bbe698dfc953e160c51d26defe61ad68885bd9960aeb3a3d5bb637afab9df216d42894c37e5f6a12f2695ff634b32323c2783c499353758316800138370720320754ddd300dd14fa78f278bcab37f219979889cbc9971ef862739a8dada59c8ff2f88f4bb269aa88e808f0771b987d68779a929d58e17290684c4035e582c8124484dc2d344395129434b711583f20ebb71579cb97bbf4850fe35f2bfcf1ec9c7e949f15c6cc1e8b7d56d2784c83c8a125fb0d0fae53649724a899364550011010001c2c0760418010800200502680b932f021b0c162104cccb5aa9f6e1141c943165f1db18b18cbcf70487000a0910db18b18cbcf704873c3807fd1e3e54a16fc879fc006af060de9216a761188b73fcaa617383feb632b80bcbbf362ea4381bd15e58cffa5ec03da0cd50e4adf37be5c81a66d6a22b9835cbb9c219ecd7426547e6a8ec35839d76795aa448a544bc4a5ecfea0284c1ee576a3dc9fdd41beb54f3f60283451b1d292bddda076e1c02b82d957708dcea5f6fb4faf72f69bdff01ed89468e9870e1a081dac09ccc0b9590ac12e7b85008838e8f9aafcfb2bdcc63085a70819c4f6b8b77cff5716af43c834d114a22745eea504b90c431abadb06ba979021726de29fa09523254ff88d3a9a94ba22c46ba5eb4919ca3c8d1f58b1349c5dd1747afb88067dd2ee258b07b8eb0e09235da2469fcc08c79',NULL,'CCCB5AA9F6E1141C943165F1DB18B18CBCF70487',NULL,NULL,'',NULL,NULL,'',NULL); + INSERT INTO contacts VALUES(10,'','bob@example.net',16384,0,0,'','',1745589041,'',0); + INSERT INTO chats VALUES(10,120,'Group',0,'',0,'','g=1745609548',0,0,0,0,0,1745609547,0,NULL,1); + INSERT INTO chats_contacts VALUES(10,1,1745609547,0); + INSERT INTO chats_contacts VALUES(10,10,1745609547,0);"#, + )?)).await?; + t.sql.run_migrations(&t).await?; + + //std::thread::sleep(std::time::Duration::from_secs(1000)); + let email_bob_id = Contact::lookup_id_by_addr(&t, "bob@example.net", Origin::Hidden) + .await? + .unwrap(); + let email_bob = Contact::get_by_id(&t, email_bob_id).await?; + assert_eq!(email_bob.origin, Origin::OutgoingTo); + assert_eq!(email_bob.e2ee_avail(&t).await?, false); + assert_eq!(email_bob.fingerprint(), None); + assert_eq!(email_bob.get_verifier_id(&t).await?, None); + + let bob_chat_contacts = chat::get_chat_contacts(&t, ChatId::new(10)).await?; + dbg!(&bob_chat_contacts); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_pgp_contacts_migration_email2() -> Result<()> { + let t = STOP_MIGRATIONS_AT + .scope(131, async move { TestContext::new_alice().await }) + .await; + + t.sql.call_write(|conn| Ok(conn.execute_batch(r#" + INSERT INTO acpeerstates VALUES(1,'bob@example.net',0,0,NULL,20,1745589039,X'c6c04d045e30c757010800cec0b4bfc4277c88a0d652cc937d5cd66f2f9918a3e96a63d3bdd8f41858277f4075101680e7ffcf8c0cdb2b988a8a8e903449996a0cc93e45cf07225c0084549b44f5eada83b42bf19be1fddd8117a478bf5d639e270f64a210134aa52db113b4a4525e0ef3e2313990ac498762858349005f0aba3065dbe730095b27d26360e9e070c793c5cd23c663ece6cd7bc850bed4e5aee1fc160b250cdf0cb527374a4dc0d6af2ad292f9a015d52a27ba490e4d47153b7ec7db6f4252b7ba7f415e2470bf4bb4cc34ae23c7831ff7512c0e142fd3eaeaf9899816a67b504fb04d4f03b573793489476a28257313ea8d80987f0f3d47d192fdce896ba1ecb339152a470011010001cd113c626f62406578616d706c652e6e65743ec2c08b0410010800350219010502680b932f021b03040b090807061508090a0b02031602010127162104cccb5aa9f6e1141c943165f1db18b18cbcf70487000a0910db18b18cbcf70487069707ff53fae1e7d40cf1b2b0ea22c1cadd735b16fbdc4c0571fc88b9c489bb2023fce8e197880c4d579d67fa75124ae696fecc17cd5815362e00601e9240d10e0a46bfc0567b88312a41e56bedb045482de61279eb7d10cf15b23e56dc254084401eeaac0780f7ca912f6f9e3d4e4b3f82b1a0fc3ee6600e5367549dbc83242743dee435287c1ba1db604f4d7416780a5d43fe8047338866715a9081285797b96cb9340822d04331121646188e3c9e9bf209611fe9f72bf5df3f0cfdf46d698566ae5ef75e8fa05f5d760e22e592c61e2a48dffeff8cec2f425a5c04951df78f68362f475ba9a8f15e4f588d85f8738815d92d8ccd876833c1683927dd28f5ede9da8ecec04d045e30c757010800a207812db22369e2482375b6a71b2ef9212eb1090957291b1980edab25d5f970598ac638184d244dac0ae66a9287eac3aaab82c438185814539c667010aa219e3d8d1bbe698dfc953e160c51d26defe61ad68885bd9960aeb3a3d5bb637afab9df216d42894c37e5f6a12f2695ff634b32323c2783c499353758316800138370720320754ddd300dd14fa78f278bcab37f219979889cbc9971ef862739a8dada59c8ff2f88f4bb269aa88e808f0771b987d68779a929d58e17290684c4035e582c8124484dc2d344395129434b711583f20ebb71579cb97bbf4850fe35f2bfcf1ec9c7e949f15c6cc1e8b7d56d2784c83c8a125fb0d0fae53649724a899364550011010001c2c0760418010800200502680b932f021b0c162104cccb5aa9f6e1141c943165f1db18b18cbcf70487000a0910db18b18cbcf704873c3807fd1e3e54a16fc879fc006af060de9216a761188b73fcaa617383feb632b80bcbbf362ea4381bd15e58cffa5ec03da0cd50e4adf37be5c81a66d6a22b9835cbb9c219ecd7426547e6a8ec35839d76795aa448a544bc4a5ecfea0284c1ee576a3dc9fdd41beb54f3f60283451b1d292bddda076e1c02b82d957708dcea5f6fb4faf72f69bdff01ed89468e9870e1a081dac09ccc0b9590ac12e7b85008838e8f9aafcfb2bdcc63085a70819c4f6b8b77cff5716af43c834d114a22745eea504b90c431abadb06ba979021726de29fa09523254ff88d3a9a94ba22c46ba5eb4919ca3c8d1f58b1349c5dd1747afb88067dd2ee258b07b8eb0e09235da2469fcc08c79',NULL,'CCCB5AA9F6E1141C943165F1DB18B18CBCF70487',NULL,NULL,'',NULL,NULL,'',NULL); + INSERT INTO contacts VALUES(10,'','bob@example.net',16384,0,0,'','',1745589041,'',0); + INSERT INTO chats VALUES(10,100,'bob@example.net',0,'',0,'','',0,0,0,0,0,1745589039,0,NULL,0); + INSERT INTO chats_contacts VALUES(10,10,0,0);"#, + )?)).await?; + t.sql.run_migrations(&t).await?; + + //std::thread::sleep(std::time::Duration::from_secs(1000)); + let email_bob_id = Contact::lookup_id_by_addr(&t, "bob@example.net", Origin::Hidden) + .await? + .unwrap(); + let email_bob = Contact::get_by_id(&t, email_bob_id).await?; + assert_eq!(email_bob.origin, Origin::OutgoingTo); // Email bob is in no chats, so, contact is hidden + assert_eq!(email_bob.e2ee_avail(&t).await?, false); + assert_eq!(email_bob.fingerprint(), None); + assert_eq!(email_bob.get_verifier_id(&t).await?, None); + + let bob_chat_contacts = chat::get_chat_contacts(&t, ChatId::new(10)).await?; + dbg!(&bob_chat_contacts); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_pgp_contacts_migration_verified() -> Result<()> { let t = STOP_MIGRATIONS_AT From 42e7d17dacca678130aa306a8b7b40acb8cf60f9 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Sat, 26 Apr 2025 21:08:50 +0200 Subject: [PATCH 201/381] Fix some things, now my personal database is correctly migrated now --- src/sql/migrations.rs | 47 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index d57610c775..e41ffd475a 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1291,7 +1291,7 @@ fn migrate_pgp_contacts( ) STRICT; CREATE INDEX public_key_index ON public_keys (fingerprint); - INSERT INTO public_keys (fingerprint, public_key) + INSERT OR IGNORE INTO public_keys (fingerprint, public_key) SELECT public_key_fingerprint, public_key FROM acpeerstates WHERE public_key_fingerprint IS NOT NULL AND public_key IS NOT NULL; @@ -1306,7 +1306,8 @@ fn migrate_pgp_contacts( INSERT OR IGNORE INTO public_keys (fingerprint, public_key) SELECT secondary_verified_key_fingerprint, secondary_verified_key FROM acpeerstates WHERE secondary_verified_key_fingerprint IS NOT NULL AND secondary_verified_key IS NOT NULL;", - )?; + ) + .context("Creating PGP-contact tables")?; // TODO remove this commented-out code // transaction.execute( @@ -1542,6 +1543,8 @@ fn migrate_pgp_contacts( Ok((id, typ, grpid, protected)) })?; + let mut update_member_stmt = transaction + .prepare("UPDATE chats_contacts SET contact_id=? WHERE contact_id=? AND chat_id=?")?; for chat in all_chats { let (chat_id, typ, grpid, protected) = chat?; // In groups, this also contains past members @@ -1565,8 +1568,7 @@ fn migrate_pgp_contacts( // and the effect will be the same. 100 => { let Some(old_member) = old_members.first() else { - warn!(context, "1:1 chat {chat_id} doesn't contain contact"); - debug_assert!(false, "1:1 chat {chat_id} doesn't contain contact"); + info!(context, "1:1 chat {chat_id} doesn't contain contact, probably it's self or device chat"); continue; }; let Some(&new_contact) = autocrypt_pgp_contacts.get(old_member) else { @@ -1602,6 +1604,9 @@ fn migrate_pgp_contacts( // TODO it's unclear whether we want to do this: // We could also make the group unencrypted // if any peerstate is reset. + // Also, right now, if we have no key at all, + // the member will be silently removed from the group; + // maybe we should at least post an info message? .or_else(|| { autocrypt_pgp_contacts_with_reset_peerstate .get(original) @@ -1643,10 +1648,36 @@ fn migrate_pgp_contacts( for (old_member, new_member) in old_and_new_members { if let Some(new_member) = new_member { orphaned_contacts.remove(&new_member); - transaction.execute( - "UPDATE chats_contacts SET contact_id=? WHERE contact_id=? AND chat_id=?", - (new_member, old_member, chat_id), - )?; + let res = update_member_stmt.execute((new_member, old_member, chat_id)); + if res.is_err() { + // The same chat partner exists multiple times in the chat, + // with mutliple profiles which have different email addresses + // but the same key. + // We can only keep one of them. + // So, if one of them is not in the chat anymore, delete it, + // otherwise delete the one that was added least recently. + let member_to_delete: u32 = transaction.query_row( + "SELECT contact_id + FROM chats_contacts + WHERE chat_id=? AND contact_id IN (?,?) + ORDER BY add_timestamp>=remove_timestamp, add_timestamp LIMIT 1", + (chat_id, new_member, old_member), + |row| row.get(0), + )?; + info!( + context, + "Chat partner is in the chat {chat_id} multiple times. \ + Deleting {member_to_delete}, then trying to update \ + {old_member}->{new_member} again" + ); + transaction.execute( + "DELETE FROM chats_contacts WHERE chat_id=? AND contact_id=?", + (chat_id, member_to_delete), + )?; + // If we removed `old_member`, then this will be a no-op, + // which is exactly what we want in this case: + update_member_stmt.execute((new_member, old_member, chat_id))?; + } } else { transaction.execute( "DELETE FROM chats_contacts WHERE contact_id=? AND chat_id=?", From 544f23c3d2dc59338e3385f829027779b89352bf Mon Sep 17 00:00:00 2001 From: Hocuri Date: Sat, 26 Apr 2025 21:24:44 +0200 Subject: [PATCH 202/381] fix: Don't create PGP chat if none of the members can encrypt --- src/sql/migrations.rs | 64 +++++++++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 23 deletions(-) diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index e41ffd475a..7094c0afb5 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -13,7 +13,6 @@ use rusqlite::OptionalExtension; use crate::config::Config; use crate::configure::EnteredLoginParam; use crate::constants::ShowEmails; -use crate::contact::ContactId; use crate::context::Context; use crate::imap; use crate::key::DcKey; @@ -1267,11 +1266,6 @@ fn migrate_pgp_contacts( transaction: &mut rusqlite::Transaction<'_>, ) -> std::result::Result<(), anyhow::Error> { info!(context, "Starting PGP contact transition."); - let self_addr: String = transaction.query_row( - "SELECT value FROM config WHERE keyname='configured_addr'", - (), - |row| row.get(0), - )?; // =============================== Step 1: =============================== // Alter tables @@ -1309,6 +1303,22 @@ fn migrate_pgp_contacts( ) .context("Creating PGP-contact tables")?; + let Some(self_addr): Option = transaction + .query_row( + "SELECT value FROM config WHERE keyname='configured_addr'", + (), + |row| row.get(0), + ) + .optional() + .context("Step 0")? + else { + info!( + context, + "Not yet configured, no need to migrate PGP contacts" + ); + return Ok(()); + }; + // TODO remove this commented-out code // transaction.execute( // "INSERT INTO contacts (name, addr, origin, blocked, last_seen, @@ -1335,8 +1345,9 @@ fn migrate_pgp_contacts( // TODO apply verifications let mut verifications: BTreeMap = BTreeMap::new(); - let mut load_contacts_stmt = transaction.prepare( - "SELECT c.id, c.name, c.addr, c.origin, c.blocked, c.last_seen, + let mut load_contacts_stmt = transaction + .prepare( + "SELECT c.id, c.name, c.addr, c.origin, c.blocked, c.last_seen, c.authname, c.param, c.status, c.is_bot, c.selfavatar_sent, IFNULL(p.public_key, p.gossip_key), p.verified_key, p.verifier, @@ -1402,9 +1413,9 @@ fn migrate_pgp_contacts( VALUES(?,?,?,?,?,?,?,?,?,?,?)", )?; let mut fingerprint_to_id_stmt = - transaction.prepare("SELECT id FROM contacts WHERE fingerprint=?")?; - let mut original_contact_id_from_addr_stmt = - transaction.prepare("SELECT id FROM contacts WHERE addr=? AND fingerprint=''")?; + transaction.prepare("SELECT id FROM contacts WHERE fingerprint=? AND id>9")?; + let mut original_contact_id_from_addr_stmt = transaction + .prepare("SELECT id FROM contacts WHERE addr=? AND fingerprint='' AND id>9")?; for row in all_email_contacts? { let ( @@ -1454,11 +1465,19 @@ fn migrate_pgp_contacts( ); Ok(id) }; - let mut original_contact_id_from_addr = |addr: &str| -> rusqlite::Result { - if addr_cmp(addr, &self_addr) { + let mut original_contact_id_from_addr = |addr: &str| -> Result { + if addr_cmp(addr, &self_addr) || addr.is_empty() { + // TODO not sure what to do when addr is empty: + // This marks all contacts that were verified + // before we recorded who introduced whom + // as directly verified. + // An alternative would be putting `new_id` here. + return Ok(1); // ContactId::SELF } - original_contact_id_from_addr_stmt.query_row((addr,), |row| row.get(0)) + original_contact_id_from_addr_stmt + .query_row((addr,), |row| row.get(0)) + .with_context(|| format!("Verifier '{addr}' not found in original contacts")) }; let Some(autocrypt_key) = autocrypt_key else { @@ -1479,7 +1498,7 @@ fn migrate_pgp_contacts( }; let new_id = insert_contact(verified_key)?; verified_pgp_contacts.insert(original_id.try_into()?, new_id); - let verifier_id: u32 = original_contact_id_from_addr(&verifier)?; + let verifier_id = original_contact_id_from_addr(&verifier)?; verifications.insert(new_id, verifier_id); let Some(secondary_verified_key) = secondary_verified_key else { @@ -1552,8 +1571,8 @@ fn migrate_pgp_contacts( .query_map((chat_id,), |row| row.get::<_, u32>(0))? .collect::, rusqlite::Error>>()?; - let mut keep_email_contacts = || { - info!(context, "Chat {chat_id} will be an unencrypted chat with contacts identified by email address."); + let mut keep_email_contacts = |reason: &str| { + info!(context, "Chat {chat_id} will be an unencrypted chat with contacts identified by email address: {reason}"); for m in &old_members { orphaned_contacts.remove(m); } @@ -1572,8 +1591,7 @@ fn migrate_pgp_contacts( continue; }; let Some(&new_contact) = autocrypt_pgp_contacts.get(old_member) else { - // No peerstate, or peerstate in "reset" state. Keep email contact. - keep_email_contacts(); + keep_email_contacts("No peerstate, or peerstate in 'reset' state"); continue; }; vec![(*old_member, Some(new_contact))] @@ -1584,7 +1602,7 @@ fn migrate_pgp_contacts( if grpid.is_empty() { // Ad-hoc group that has empty Chat-Group-ID // because it was created in response to receiving a non-chat email. - keep_email_contacts(); + keep_email_contacts("Empty chat-Group-ID"); continue; } else if protected == 1 { old_members @@ -1620,7 +1638,7 @@ fn migrate_pgp_contacts( // Mailinglist | Broadcast list 140 | 160 => { - keep_email_contacts(); + keep_email_contacts("Mailinglist/Broadcast"); continue; } @@ -1630,8 +1648,8 @@ fn migrate_pgp_contacts( } }; - if old_and_new_members.is_empty() { - keep_email_contacts(); + if old_and_new_members.iter().all(|(_old, new)| *new == None) { + keep_email_contacts("All contacts in chat are e-mail contacts"); continue; } From f160d68b7f5e0c11c4154674d887a24ad23cc4e9 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Tue, 29 Apr 2025 11:44:22 +0200 Subject: [PATCH 203/381] feat: On error, report which SQL statement failed --- src/sql/migrations.rs | 167 +++++++++++++++++++++++++----------------- 1 file changed, 101 insertions(+), 66 deletions(-) diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 7094c0afb5..0dfbb53860 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1355,7 +1355,8 @@ fn migrate_pgp_contacts( FROM contacts c INNER JOIN acpeerstates p ON c.addr=p.addr WHERE c.id > 9", - )?; + ) + .context("Step 2")?; let all_email_contacts: rusqlite::Result> = load_contacts_stmt .query_map((), |row| { @@ -1404,18 +1405,23 @@ fn migrate_pgp_contacts( secondary_verifier, prefer_encrypt, )) - })? + }) + .context("Step 3")? .collect(); - let mut insert_contact_stmt = transaction.prepare( - "INSERT INTO contacts (name, addr, origin, blocked, last_seen, + let mut insert_contact_stmt = transaction + .prepare( + "INSERT INTO contacts (name, addr, origin, blocked, last_seen, authname, param, status, is_bot, selfavatar_sent, fingerprint) VALUES(?,?,?,?,?,?,?,?,?,?,?)", - )?; - let mut fingerprint_to_id_stmt = - transaction.prepare("SELECT id FROM contacts WHERE fingerprint=? AND id>9")?; + ) + .context("Step 4")?; + let mut fingerprint_to_id_stmt = transaction + .prepare("SELECT id FROM contacts WHERE fingerprint=? AND id>9") + .context("Step 5")?; let mut original_contact_id_from_addr_stmt = transaction - .prepare("SELECT id FROM contacts WHERE addr=? AND fingerprint='' AND id>9")?; + .prepare("SELECT id FROM contacts WHERE addr=? AND fingerprint='' AND id>9") + .context("Step 6")?; for row in all_email_contacts? { let ( @@ -1441,24 +1447,30 @@ fn migrate_pgp_contacts( let fingerprint = key.dc_fingerprint().hex(); let existing_contact_id: Option = fingerprint_to_id_stmt .query_row((&fingerprint,), |row| row.get(0)) - .optional()?; + .optional() + .context("Step 7")?; if let Some(existing_contact_id) = existing_contact_id { return Ok(existing_contact_id); } - insert_contact_stmt.execute(( - &name, - &addr, - origin, - blocked, - last_seen, - &authname, - ¶m, - &status, - is_bot, - selfavatar_sent, - fingerprint.clone(), - ))?; - let id = transaction.last_insert_rowid().try_into()?; + insert_contact_stmt + .execute(( + &name, + &addr, + origin, + blocked, + last_seen, + &authname, + ¶m, + &status, + is_bot, + selfavatar_sent, + fingerprint.clone(), + )) + .context("Step 8")?; + let id = transaction + .last_insert_rowid() + .try_into() + .context("Step 9")?; info!( context, "Inserted new contact id={id} name='{name}' addr='{addr}' fingerprint={fingerprint}" @@ -1483,29 +1495,31 @@ fn migrate_pgp_contacts( let Some(autocrypt_key) = autocrypt_key else { continue; }; - let new_id = insert_contact(autocrypt_key)?; + let new_id = insert_contact(autocrypt_key).context("Step 10")?; // prefer_encrypt == 20 would mean EncryptPreference::Reset, // i.e. we shouldn't encrypt if possible. if prefer_encrypt != 20 { - autocrypt_pgp_contacts.insert(original_id.try_into()?, new_id); + autocrypt_pgp_contacts.insert(original_id.try_into().context("Step 11")?, new_id); } else { - autocrypt_pgp_contacts_with_reset_peerstate.insert(original_id.try_into()?, new_id); + autocrypt_pgp_contacts_with_reset_peerstate + .insert(original_id.try_into().context("Step 12")?, new_id); } let Some(verified_key) = verified_key else { continue; }; - let new_id = insert_contact(verified_key)?; - verified_pgp_contacts.insert(original_id.try_into()?, new_id); - let verifier_id = original_contact_id_from_addr(&verifier)?; + let new_id = insert_contact(verified_key).context("Step 13")?; + verified_pgp_contacts.insert(original_id.try_into().context("Step 14")?, new_id); + let verifier_id = original_contact_id_from_addr(&verifier).context("Step 15")?; verifications.insert(new_id, verifier_id); let Some(secondary_verified_key) = secondary_verified_key else { continue; }; - let new_id = insert_contact(secondary_verified_key)?; - let verifier_id: u32 = original_contact_id_from_addr(&secondary_verifier)?; + let new_id = insert_contact(secondary_verified_key).context("Step 16")?; + let verifier_id: u32 = + original_contact_id_from_addr(&secondary_verifier).context("Step 17")?; // Only use secondary verification if there is no primary verification: verifications.entry(new_id).or_insert(verifier_id); } @@ -1528,10 +1542,12 @@ fn migrate_pgp_contacts( verified_pgp_contacts.get(&verifier_original_contact) }; if let Some(&verifier) = verifier { - transaction.execute( - "UPDATE contacts SET verifier=? WHERE id=?", - (verifier, new_contact), - )?; + transaction + .execute( + "UPDATE contacts SET verifier=? WHERE id=?", + (verifier, new_contact), + ) + .context("Step 18")?; } } info!(context, "Migrated verifications: {verifications:?}"); @@ -1542,34 +1558,43 @@ fn migrate_pgp_contacts( // In the process, track the set of contacts which remained no any chat at all // in a `BTreeSet`, which initially contains all contact ids let mut orphaned_contacts: BTreeSet = transaction - .prepare("SELECT id FROM contacts WHERE id>9")? - .query_map((), |row| row.get::(0))? - .collect::, rusqlite::Error>>()?; + .prepare("SELECT id FROM contacts WHERE id>9") + .context("Step 19")? + .query_map((), |row| row.get::(0)) + .context("Step 20")? + .collect::, rusqlite::Error>>() + .context("Step 21")?; { - let mut stmt = transaction.prepare( - "SELECT c.id, c.type, c.grpid, c.protected + let mut stmt = transaction + .prepare( + "SELECT c.id, c.type, c.grpid, c.protected FROM chats c WHERE id>9", - )?; + ) + .context("Step 22")?; let mut load_chat_contacts_stmt = transaction .prepare("SELECT contact_id FROM chats_contacts WHERE chat_id=? AND contact_id>9")?; - let all_chats = stmt.query_map((), |row| { - let id: u32 = row.get(0)?; - let typ: u32 = row.get(1)?; - let grpid: String = row.get(2)?; - let protected: u32 = row.get(3)?; - Ok((id, typ, grpid, protected)) - })?; + let all_chats = stmt + .query_map((), |row| { + let id: u32 = row.get(0)?; + let typ: u32 = row.get(1)?; + let grpid: String = row.get(2)?; + let protected: u32 = row.get(3)?; + Ok((id, typ, grpid, protected)) + }) + .context("Step 23")?; let mut update_member_stmt = transaction .prepare("UPDATE chats_contacts SET contact_id=? WHERE contact_id=? AND chat_id=?")?; for chat in all_chats { - let (chat_id, typ, grpid, protected) = chat?; + let (chat_id, typ, grpid, protected) = chat.context("Step 24")?; // In groups, this also contains past members let old_members: Vec = load_chat_contacts_stmt - .query_map((chat_id,), |row| row.get::<_, u32>(0))? - .collect::, rusqlite::Error>>()?; + .query_map((chat_id,), |row| row.get::<_, u32>(0)) + .context("Step 25")? + .collect::, rusqlite::Error>>() + .context("Step 26")?; let mut keep_email_contacts = |reason: &str| { info!(context, "Chat {chat_id} will be an unencrypted chat with contacts identified by email address: {reason}"); @@ -1674,33 +1699,39 @@ fn migrate_pgp_contacts( // We can only keep one of them. // So, if one of them is not in the chat anymore, delete it, // otherwise delete the one that was added least recently. - let member_to_delete: u32 = transaction.query_row( - "SELECT contact_id + let member_to_delete: u32 = transaction + .query_row( + "SELECT contact_id FROM chats_contacts WHERE chat_id=? AND contact_id IN (?,?) ORDER BY add_timestamp>=remove_timestamp, add_timestamp LIMIT 1", - (chat_id, new_member, old_member), - |row| row.get(0), - )?; + (chat_id, new_member, old_member), + |row| row.get(0), + ) + .context("Step 27")?; info!( context, "Chat partner is in the chat {chat_id} multiple times. \ Deleting {member_to_delete}, then trying to update \ {old_member}->{new_member} again" ); - transaction.execute( - "DELETE FROM chats_contacts WHERE chat_id=? AND contact_id=?", - (chat_id, member_to_delete), - )?; + transaction + .execute( + "DELETE FROM chats_contacts WHERE chat_id=? AND contact_id=?", + (chat_id, member_to_delete), + ) + .context("Step 28")?; // If we removed `old_member`, then this will be a no-op, // which is exactly what we want in this case: update_member_stmt.execute((new_member, old_member, chat_id))?; } } else { - transaction.execute( - "DELETE FROM chats_contacts WHERE contact_id=? AND chat_id=?", - (old_member, chat_id), - )?; + transaction + .execute( + "DELETE FROM chats_contacts WHERE contact_id=? AND chat_id=?", + (old_member, chat_id), + ) + .context("Step 29")?; } } } @@ -1711,9 +1742,13 @@ fn migrate_pgp_contacts( context, "Marking contacts which remained in no chat at all as hidden: {orphaned_contacts:?}" ); - let mut mark_as_hidden_stmt = transaction.prepare("UPDATE contacts SET origin=? WHERE id=?")?; + let mut mark_as_hidden_stmt = transaction + .prepare("UPDATE contacts SET origin=? WHERE id=?") + .context("Step 30")?; for contact in orphaned_contacts { - mark_as_hidden_stmt.execute((0x8, contact))?; + mark_as_hidden_stmt + .execute((0x8, contact)) + .context("Step 31")?; } Ok(()) From 6e2f30e703af4e20252a66792c7adecd789a03df Mon Sep 17 00:00:00 2001 From: Hocuri Date: Tue, 29 Apr 2025 11:44:46 +0200 Subject: [PATCH 204/381] feat: Allow to open context even though migrations failed --- src/sql.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/sql.rs b/src/sql.rs index 0ecd6e85c0..7518b3d991 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -8,7 +8,10 @@ use rusqlite::{config::DbConfig, types::ValueRef, Connection, OpenFlags, Row}; use tokio::sync::RwLock; use crate::blob::BlobObject; -use crate::chat::{self, add_device_msg, update_device_icon, update_saved_messages_icon}; +use crate::chat::{ + self, add_device_msg, add_device_msg_with_importance, update_device_icon, + update_saved_messages_icon, +}; use crate::config::Config; use crate::constants::DC_CHAT_ID_TRASH; use crate::context::Context; @@ -190,7 +193,11 @@ impl Sql { async fn try_open(&self, context: &Context, dbfile: &Path, passphrase: String) -> Result<()> { *self.pool.write().await = Some(Self::new_pool(dbfile, passphrase.to_string())?); - self.run_migrations(context).await?; + if let Err(e) = self.run_migrations(context).await { + error!(context, "Running migrations failed: {e:#}"); + context.set_last_error(&format!("Updating Delta Chat failed. Please send this message to the Delta Chat developers, either at delta@merlinux.eu or at https://support.delta.chat.\n\n{e:#}")); + // TODO possibly we should make the db read-only or close it + } Ok(()) } From 0d47538dff0d430449a3ce7e89c0fe93d936e527 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Tue, 29 Apr 2025 17:19:10 +0200 Subject: [PATCH 205/381] feat: Set migration error instead of re-using last_error --- deltachat-jsonrpc/src/api.rs | 5 +++++ src/context.rs | 8 ++++++++ src/log.rs | 10 ++++++++++ src/sql.rs | 2 +- 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/deltachat-jsonrpc/src/api.rs b/deltachat-jsonrpc/src/api.rs index e7fed37380..5919a09ff2 100644 --- a/deltachat-jsonrpc/src/api.rs +++ b/deltachat-jsonrpc/src/api.rs @@ -354,6 +354,11 @@ impl CommandApi { Ok(ctx.get_blobdir().to_str().map(|s| s.to_owned())) } + async fn get_migration_error(&self, account_id: u32) -> Result> { + let ctx = self.get_context(account_id).await?; + Ok(ctx.get_migration_error()) + } + /// Copy file to blob dir. async fn copy_to_blob_dir(&self, account_id: u32, path: String) -> Result { let ctx = self.get_context(account_id).await?; diff --git a/src/context.rs b/src/context.rs index 05fd1e43a5..a1288d2240 100644 --- a/src/context.rs +++ b/src/context.rs @@ -275,6 +275,13 @@ pub struct InnerContext { /// `last_error` should be used to avoid races with the event thread. pub(crate) last_error: parking_lot::RwLock, + /// It's not possible to emit migration errors as an event, + /// because at the time of the migration, there is no event emitter yet. + /// So, this holds the error that happened during migration, if any. + /// This is necessary for the possibly-failible PGP migration, + /// which happened 2025-05, and can be removed a few releases later. + pub(crate) migration_error: parking_lot::RwLock>, + /// If debug logging is enabled, this contains all necessary information /// /// Standard RwLock instead of [`tokio::sync::RwLock`] is used @@ -444,6 +451,7 @@ impl Context { creation_time: tools::Time::now(), last_full_folder_scan: Mutex::new(None), last_error: parking_lot::RwLock::new("".to_string()), + migration_error: parking_lot::RwLock::new(None), debug_logging: std::sync::RwLock::new(None), push_subscriber, push_subscribed: AtomicBool::new(false), diff --git a/src/log.rs b/src/log.rs index b665cf242d..7ca580c80b 100644 --- a/src/log.rs +++ b/src/log.rs @@ -59,6 +59,16 @@ impl Context { let last_error = &*self.last_error.read(); last_error.clone() } + + pub fn set_migration_error(&self, error: &str) { + let mut migration_error = self.migration_error.write(); + *migration_error = Some(error.to_string()); + } + + pub fn get_migration_error(&self) -> Option { + let migration_error = &*self.migration_error.read(); + migration_error.clone() + } } pub trait LogExt diff --git a/src/sql.rs b/src/sql.rs index 7518b3d991..0be59bb738 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -195,7 +195,7 @@ impl Sql { if let Err(e) = self.run_migrations(context).await { error!(context, "Running migrations failed: {e:#}"); - context.set_last_error(&format!("Updating Delta Chat failed. Please send this message to the Delta Chat developers, either at delta@merlinux.eu or at https://support.delta.chat.\n\n{e:#}")); + context.set_migration_error(&format!("Updating Delta Chat failed. Please send this message to the Delta Chat developers, either at delta@merlinux.eu or at https://support.delta.chat.\n\n{e:#}")); // TODO possibly we should make the db read-only or close it } From 72881b22a4454f509bfa4ce160961e5c50644457 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Tue, 29 Apr 2025 18:49:41 +0200 Subject: [PATCH 206/381] Remove outdated TODOs --- src/sql.rs | 6 +++++- src/sql/migrations.rs | 21 +++++---------------- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/src/sql.rs b/src/sql.rs index 0be59bb738..0182669388 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -196,7 +196,11 @@ impl Sql { if let Err(e) = self.run_migrations(context).await { error!(context, "Running migrations failed: {e:#}"); context.set_migration_error(&format!("Updating Delta Chat failed. Please send this message to the Delta Chat developers, either at delta@merlinux.eu or at https://support.delta.chat.\n\n{e:#}")); - // TODO possibly we should make the db read-only or close it + // TODO possibly we should make the db read-only. + // We can't simply close it for two reasons: + // a. backup export would fail + // b. The UI would think that the account is unconfigured (because `is_configured()` fails) + // and remove the account } Ok(()) diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 0dfbb53860..7c7c7d49e7 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1319,15 +1319,6 @@ fn migrate_pgp_contacts( return Ok(()); }; - // TODO remove this commented-out code - // transaction.execute( - // "INSERT INTO contacts (name, addr, origin, blocked, last_seen, - // authname, param, status, is_bot) - // SELECT c.name, c.addr, c.origin, c.blocked, c.last_seen, - // c.authname, c.param, c.status, c.is_bot - // FROM contacts c - // INNER JOIN peerstates ON c.addr=acpeerstates.addr", () - // )?; // =============================== Step 2: =============================== // Create up to 3 new contacts for every contact that has a peerstate: // one from the Autocrypt key fingerprint, one from the verified key fingerprint, @@ -1336,13 +1327,11 @@ fn migrate_pgp_contacts( // one that maps to Autocrypt PGP-contact, one that maps to verified PGP-contact. let mut autocrypt_pgp_contacts: BTreeMap = BTreeMap::new(); let mut autocrypt_pgp_contacts_with_reset_peerstate: BTreeMap = BTreeMap::new(); - // TODO should secondary verified PGP keys also go in here? let mut verified_pgp_contacts: BTreeMap = BTreeMap::new(); { // This maps from the verified contact to the original contact id of the verifier. // It can't map to the verified pgp contact id, because at the time of constructing // this map, not all pgp contacts are in the database. - // TODO apply verifications let mut verifications: BTreeMap = BTreeMap::new(); let mut load_contacts_stmt = transaction @@ -1479,11 +1468,11 @@ fn migrate_pgp_contacts( }; let mut original_contact_id_from_addr = |addr: &str| -> Result { if addr_cmp(addr, &self_addr) || addr.is_empty() { - // TODO not sure what to do when addr is empty: - // This marks all contacts that were verified - // before we recorded who introduced whom - // as directly verified. - // An alternative would be putting `new_id` here. + // TODO not sure what to do when addr is empty, + // i.e. the contact was verified before we recorded who introduced whom. + // Right now, we put ContactId::SELF, i.e. mark it as directly verified by us. + // An alternative would be putting `new_id` here, + // in order to record that it's unclear who verified. return Ok(1); // ContactId::SELF } From d76237d1273f178835e670ac06d7ec885de97573 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Tue, 29 Apr 2025 18:50:09 +0200 Subject: [PATCH 207/381] fix test compilation --- src/sql/migrations/migrations_tests.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sql/migrations/migrations_tests.rs b/src/sql/migrations/migrations_tests.rs index f33f6d95cf..e49d5f2888 100644 --- a/src/sql/migrations/migrations_tests.rs +++ b/src/sql/migrations/migrations_tests.rs @@ -3,6 +3,7 @@ use crate::chat; use crate::chat::ChatId; use crate::config::Config; use crate::contact::Contact; +use crate::contact::ContactId; use crate::contact::Origin; use crate::test_utils::bob_keypair; use crate::test_utils::TestContext; From ca3d132d86facdf8052f008ed90dfd6c666198b6 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Tue, 29 Apr 2025 19:02:14 +0200 Subject: [PATCH 208/381] Make debug=1 again for test builds --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index a58a89d66b..2696995f0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ opt-level = 1 # Make anyhow `backtrace` feature useful. # With `debug = 0` there are no line numbers in the backtrace # produced with RUST_BACKTRACE=1. -debug = 'full' +debug = 1 opt-level = 0 # Always optimize dependencies. From 4e5efc8d46d3abbfc6f88daaed7db79ab4be094d Mon Sep 17 00:00:00 2001 From: Hocuri Date: Wed, 30 Apr 2025 14:17:34 +0200 Subject: [PATCH 209/381] feat: If it's unclear who verified a contact, mark it as verified by itself --- deltachat-ffi/src/lib.rs | 1 + deltachat-jsonrpc/src/api/types/contact.rs | 1 + src/contact.rs | 11 +++-- src/sql/migrations.rs | 55 ++++++++++++---------- src/sql/migrations/migrations_tests.rs | 2 +- 5 files changed, 42 insertions(+), 28 deletions(-) diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 269629fa3b..8690bc68a8 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -4362,6 +4362,7 @@ pub unsafe extern "C" fn dc_contact_get_verifier_id(contact: *mut dc_contact_t) .context("failed to get verifier") .log_err(ctx) .unwrap_or_default() + .unwrap_or_default() .unwrap_or_default(); verifier_contact_id.to_u32() diff --git a/deltachat-jsonrpc/src/api/types/contact.rs b/deltachat-jsonrpc/src/api/types/contact.rs index 4300f19540..513ad39e75 100644 --- a/deltachat-jsonrpc/src/api/types/contact.rs +++ b/deltachat-jsonrpc/src/api/types/contact.rs @@ -77,6 +77,7 @@ impl ContactObject { let verifier_id = contact .get_verifier_id(context) .await? + .flatten() .map(|contact_id| contact_id.to_u32()); Ok(ContactObject { diff --git a/src/contact.rs b/src/contact.rs index 7180c928df..88643a9c73 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -1547,11 +1547,14 @@ impl Contact { /// Returns the `ContactId` that verified the contact. /// - /// If the function returns non-zero result, + /// If this returns Some(_), /// display green checkmark in the profile and "Introduced by ..." line /// with the name and address of the contact /// formatted by [Self::get_name_n_addr]. - pub async fn get_verifier_id(&self, context: &Context) -> Result> { + /// + /// If this returns `Some(None)`, then the contact is verified, + /// but it's unclear by whom. + pub async fn get_verifier_id(&self, context: &Context) -> Result>> { let verifier_id: u32 = context .sql .query_get_value("SELECT verifier FROM contacts WHERE id=?", (self.id,)) @@ -1560,8 +1563,10 @@ impl Contact { if verifier_id == 0 { Ok(None) + } else if verifier_id == self.id.to_u32() { + Ok(Some(None)) } else { - Ok(Some(ContactId::new(verifier_id))) + Ok(Some(Some(ContactId::new(verifier_id)))) } } diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 7c7c7d49e7..68f13d85bf 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1466,19 +1466,16 @@ fn migrate_pgp_contacts( ); Ok(id) }; - let mut original_contact_id_from_addr = |addr: &str| -> Result { - if addr_cmp(addr, &self_addr) || addr.is_empty() { - // TODO not sure what to do when addr is empty, - // i.e. the contact was verified before we recorded who introduced whom. - // Right now, we put ContactId::SELF, i.e. mark it as directly verified by us. - // An alternative would be putting `new_id` here, - // in order to record that it's unclear who verified. - - return Ok(1); // ContactId::SELF + let mut original_contact_id_from_addr = |addr: &str, default: u32| -> Result { + if addr_cmp(addr, &self_addr) { + Ok(1) // ContactId::SELF + } else if addr.is_empty() { + Ok(default) + } else { + original_contact_id_from_addr_stmt + .query_row((addr,), |row| row.get(0)) + .with_context(|| format!("Original contact '{addr}' not found")) } - original_contact_id_from_addr_stmt - .query_row((addr,), |row| row.get(0)) - .with_context(|| format!("Verifier '{addr}' not found in original contacts")) }; let Some(autocrypt_key) = autocrypt_key else { @@ -1500,7 +1497,11 @@ fn migrate_pgp_contacts( }; let new_id = insert_contact(verified_key).context("Step 13")?; verified_pgp_contacts.insert(original_id.try_into().context("Step 14")?, new_id); - let verifier_id = original_contact_id_from_addr(&verifier).context("Step 15")?; + // If the original verifier is unknown, we represent this in the database + // by putting `new_id` into the place of the verifier, + // i.e. we say that this contact verified itself. + let verifier_id = + original_contact_id_from_addr(&verifier, new_id).context("Step 15")?; verifications.insert(new_id, verifier_id); let Some(secondary_verified_key) = secondary_verified_key else { @@ -1508,7 +1509,7 @@ fn migrate_pgp_contacts( }; let new_id = insert_contact(secondary_verified_key).context("Step 16")?; let verifier_id: u32 = - original_contact_id_from_addr(&secondary_verifier).context("Step 17")?; + original_contact_id_from_addr(&secondary_verifier, new_id).context("Step 17")?; // Only use secondary verification if there is no primary verification: verifications.entry(new_id).or_insert(verifier_id); } @@ -1524,20 +1525,26 @@ fn migrate_pgp_contacts( for (&new_contact, &verifier_original_contact) in &verifications { let verifier = if verifier_original_contact == 1 { - Some(&1) // Verified by ContactId::SELF + 1 // Verified by ContactId::SELF + } else if verifier_original_contact == new_contact { + new_contact // unkwnown verifier } else { // `verifications` contains the original contact id. // We need to get the new, verified-pgp-identified contact id. - verified_pgp_contacts.get(&verifier_original_contact) + match verified_pgp_contacts.get(&verifier_original_contact) { + Some(v) => *v, + None => { + warn!(context, "Couldn't find PGP-contact for {verifier_original_contact} who verified {new_contact}"); + continue; + } + } }; - if let Some(&verifier) = verifier { - transaction - .execute( - "UPDATE contacts SET verifier=? WHERE id=?", - (verifier, new_contact), - ) - .context("Step 18")?; - } + transaction + .execute( + "UPDATE contacts SET verifier=? WHERE id=?", + (verifier, new_contact), + ) + .context("Step 18")?; } info!(context, "Migrated verifications: {verifications:?}"); } diff --git a/src/sql/migrations/migrations_tests.rs b/src/sql/migrations/migrations_tests.rs index e49d5f2888..46fed33382 100644 --- a/src/sql/migrations/migrations_tests.rs +++ b/src/sql/migrations/migrations_tests.rs @@ -171,7 +171,7 @@ async fn test_pgp_contacts_migration_verified() -> Result<()> { pgp_bob.fingerprint().unwrap(), pgp_bob.public_key(&t).await?.unwrap().dc_fingerprint() ); - assert_eq!(pgp_bob.get_verifier_id(&t).await?.unwrap(), ContactId::SELF); + assert_eq!(pgp_bob.get_verifier_id(&t).await?, Some(None)); Ok(()) } From aefb1c8e66b0893b7ec278367b83bd068c60a714 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Thu, 1 May 2025 12:10:20 +0200 Subject: [PATCH 210/381] Resolve TODO --- src/sql.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/sql.rs b/src/sql.rs index 0182669388..9755827b0d 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -195,12 +195,16 @@ impl Sql { if let Err(e) = self.run_migrations(context).await { error!(context, "Running migrations failed: {e:#}"); + // Emiting an error event probably doesn't work + // because we are in the process of opening the context, + // so there is no event emitter yet. + // So, try to report the error in other ways: + eprintln!("Running migrations failed: {e:#}"); context.set_migration_error(&format!("Updating Delta Chat failed. Please send this message to the Delta Chat developers, either at delta@merlinux.eu or at https://support.delta.chat.\n\n{e:#}")); - // TODO possibly we should make the db read-only. - // We can't simply close it for two reasons: + // We can't simply close the db for two reasons: // a. backup export would fail // b. The UI would think that the account is unconfigured (because `is_configured()` fails) - // and remove the account + // and remove the account when the user presses "Back" } Ok(()) @@ -274,10 +278,7 @@ impl Sql { } let passphrase_nonempty = !passphrase.is_empty(); - if let Err(err) = self.try_open(context, &self.dbfile, passphrase).await { - self.close().await; - return Err(err); - } + self.try_open(context, &self.dbfile, passphrase).await?; info!(context, "Opened database {:?}.", self.dbfile); *self.is_encrypted.write().await = Some(passphrase_nonempty); From d93556bf600e5967e7efb3583ad00915b1de33b9 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Fri, 2 May 2025 20:55:04 +0200 Subject: [PATCH 211/381] Add one more info statement --- src/sql/migrations.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 68f13d85bf..f4847922f9 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1722,6 +1722,7 @@ fn migrate_pgp_contacts( update_member_stmt.execute((new_member, old_member, chat_id))?; } } else { + info!(context, "Old member {old_member} in chat {chat_id} can't be upgraded to PGP-contact, removing them"); transaction .execute( "DELETE FROM chats_contacts WHERE contact_id=? AND chat_id=?", From 952c259c1a33b0fa161b1f4ec6c5b8dc427bcfe9 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Mon, 5 May 2025 16:16:56 +0200 Subject: [PATCH 212/381] clippy, formatting --- src/sql.rs | 2 +- src/sql/migrations.rs | 7 +++---- src/sql/migrations/migrations_tests.rs | 1 - 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/sql.rs b/src/sql.rs index 9755827b0d..279b63adc3 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -9,7 +9,7 @@ use tokio::sync::RwLock; use crate::blob::BlobObject; use crate::chat::{ - self, add_device_msg, add_device_msg_with_importance, update_device_icon, + self, add_device_msg, update_device_icon, update_saved_messages_icon, }; use crate::config::Config; diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index f4847922f9..debdf5163b 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1401,8 +1401,8 @@ fn migrate_pgp_contacts( let mut insert_contact_stmt = transaction .prepare( "INSERT INTO contacts (name, addr, origin, blocked, last_seen, - authname, param, status, is_bot, selfavatar_sent, fingerprint) - VALUES(?,?,?,?,?,?,?,?,?,?,?)", + authname, param, status, is_bot, selfavatar_sent, fingerprint) + VALUES(?,?,?,?,?,?,?,?,?,?,?)", ) .context("Step 4")?; let mut fingerprint_to_id_stmt = transaction @@ -1560,7 +1560,6 @@ fn migrate_pgp_contacts( .context("Step 20")? .collect::, rusqlite::Error>>() .context("Step 21")?; - { let mut stmt = transaction .prepare( @@ -1669,7 +1668,7 @@ fn migrate_pgp_contacts( } }; - if old_and_new_members.iter().all(|(_old, new)| *new == None) { + if old_and_new_members.iter().all(|(_old, new)| new.is_none()) { keep_email_contacts("All contacts in chat are e-mail contacts"); continue; } diff --git a/src/sql/migrations/migrations_tests.rs b/src/sql/migrations/migrations_tests.rs index 46fed33382..7ad6ac74d6 100644 --- a/src/sql/migrations/migrations_tests.rs +++ b/src/sql/migrations/migrations_tests.rs @@ -5,7 +5,6 @@ use crate::config::Config; use crate::contact::Contact; use crate::contact::ContactId; use crate::contact::Origin; -use crate::test_utils::bob_keypair; use crate::test_utils::TestContext; use crate::tools; From b8de6af05589bf53c8ddde4fd0c818276f2fec9c Mon Sep 17 00:00:00 2001 From: Hocuri Date: Mon, 5 May 2025 16:21:16 +0200 Subject: [PATCH 213/381] Small test improvements --- src/sql/migrations/migrations_tests.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/sql/migrations/migrations_tests.rs b/src/sql/migrations/migrations_tests.rs index 7ad6ac74d6..026758e10d 100644 --- a/src/sql/migrations/migrations_tests.rs +++ b/src/sql/migrations/migrations_tests.rs @@ -81,7 +81,6 @@ async fn test_pgp_contacts_migration_email1() -> Result<()> { )?)).await?; t.sql.run_migrations(&t).await?; - //std::thread::sleep(std::time::Duration::from_secs(1000)); let email_bob_id = Contact::lookup_id_by_addr(&t, "bob@example.net", Origin::Hidden) .await? .unwrap(); @@ -111,7 +110,6 @@ async fn test_pgp_contacts_migration_email2() -> Result<()> { )?)).await?; t.sql.run_migrations(&t).await?; - //std::thread::sleep(std::time::Duration::from_secs(1000)); let email_bob_id = Contact::lookup_id_by_addr(&t, "bob@example.net", Origin::Hidden) .await? .unwrap(); @@ -145,7 +143,6 @@ async fn test_pgp_contacts_migration_verified() -> Result<()> { )?)).await?; t.sql.run_migrations(&t).await?; - //std::thread::sleep(std::time::Duration::from_secs(1000)); let email_bob_id = Contact::lookup_id_by_addr(&t, "bob@example.net", Origin::Hidden) .await? .unwrap(); @@ -156,12 +153,12 @@ async fn test_pgp_contacts_migration_verified() -> Result<()> { assert_eq!(email_bob.fingerprint(), None); assert_eq!(email_bob.get_verifier_id(&t).await?, None); - let bob_chat_contacts = chat::get_chat_contacts(&t, ChatId::new(10)).await?; + let mut bob_chat_contacts = chat::get_chat_contacts(&t, ChatId::new(10)).await?; assert_eq!(bob_chat_contacts.len(), 2); - dbg!(&bob_chat_contacts); - let pgp_bob_id = ContactId::new(11); - assert!(bob_chat_contacts.contains(&pgp_bob_id)); - assert!(bob_chat_contacts.contains(&ContactId::SELF)); + bob_chat_contacts.retain(|c| *c != ContactId::SELF); + assert_eq!(bob_chat_contacts.len(), 1); + let pgp_bob_id = bob_chat_contacts[0]; + let pgp_bob = Contact::get_by_id(&t, pgp_bob_id).await?; dbg!(&pgp_bob); assert_eq!(pgp_bob.origin, Origin::OutgoingTo); From 640035f6b6bc3e9e3fdfe4dd5bf061669d583a1b Mon Sep 17 00:00:00 2001 From: Hocuri Date: Mon, 5 May 2025 16:31:12 +0200 Subject: [PATCH 214/381] fix: Prefer peerstate with newer last_seen if multiple keys have the same address --- src/sql/migrations.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index debdf5163b..475b9b73bc 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1337,13 +1337,14 @@ fn migrate_pgp_contacts( let mut load_contacts_stmt = transaction .prepare( "SELECT c.id, c.name, c.addr, c.origin, c.blocked, c.last_seen, - c.authname, c.param, c.status, c.is_bot, c.selfavatar_sent, - IFNULL(p.public_key, p.gossip_key), - p.verified_key, p.verifier, - p.secondary_verified_key, p.secondary_verifier, p.prefer_encrypted - FROM contacts c - INNER JOIN acpeerstates p ON c.addr=p.addr - WHERE c.id > 9", + c.authname, c.param, c.status, c.is_bot, c.selfavatar_sent, + IFNULL(p.public_key, p.gossip_key), + p.verified_key, p.verifier, + p.secondary_verified_key, p.secondary_verifier, p.prefer_encrypted + FROM contacts c + INNER JOIN acpeerstates p ON c.addr=p.addr + WHERE c.id > 9 + ORDER BY p.last_seen DESC", ) .context("Step 2")?; From c8fb0c9775dea3b325b6db810e523cdef97c693a Mon Sep 17 00:00:00 2001 From: Hocuri Date: Tue, 6 May 2025 11:42:00 +0200 Subject: [PATCH 215/381] small style tweaks --- src/sql.rs | 5 +---- src/sql/migrations.rs | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/sql.rs b/src/sql.rs index 279b63adc3..6c46dbab4a 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -8,10 +8,7 @@ use rusqlite::{config::DbConfig, types::ValueRef, Connection, OpenFlags, Row}; use tokio::sync::RwLock; use crate::blob::BlobObject; -use crate::chat::{ - self, add_device_msg, update_device_icon, - update_saved_messages_icon, -}; +use crate::chat::{self, add_device_msg, update_device_icon, update_saved_messages_icon}; use crate::config::Config; use crate::constants::DC_CHAT_ID_TRASH; use crate::context::Context; diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 475b9b73bc..249cf17116 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1569,8 +1569,6 @@ fn migrate_pgp_contacts( WHERE id>9", ) .context("Step 22")?; - let mut load_chat_contacts_stmt = transaction - .prepare("SELECT contact_id FROM chats_contacts WHERE chat_id=? AND contact_id>9")?; let all_chats = stmt .query_map((), |row| { let id: u32 = row.get(0)?; @@ -1580,6 +1578,8 @@ fn migrate_pgp_contacts( Ok((id, typ, grpid, protected)) }) .context("Step 23")?; + let mut load_chat_contacts_stmt = transaction + .prepare("SELECT contact_id FROM chats_contacts WHERE chat_id=? AND contact_id>9")?; let mut update_member_stmt = transaction .prepare("UPDATE chats_contacts SET contact_id=? WHERE contact_id=? AND chat_id=?")?; From 3b22df4bbb7d33e80617ca09ae8ec7d61357746d Mon Sep 17 00:00:00 2001 From: Hocuri Date: Tue, 6 May 2025 12:08:54 +0200 Subject: [PATCH 216/381] fix: Keep email email contact in 1:1 chat if PGP contact has a different addr If a contact did AEAP in the past, then it can happen that there are multiple contacts with the same email address but different keys. In this case, we should make sure that we don't send messages to a different email address all of a sudden, and that there aren't two 1:1 chats with the same contact. Therefore, if the PGP contact has a different email address than the email contact, we keep the email contact in 1:1 chats. --- src/sql/migrations.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 249cf17116..72ad692884 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1583,6 +1583,8 @@ fn migrate_pgp_contacts( let mut update_member_stmt = transaction .prepare("UPDATE chats_contacts SET contact_id=? WHERE contact_id=? AND chat_id=?")?; + let mut addr_cmp_stmt = transaction + .prepare("SELECT c.addr=d.addr FROM contacts c, contacts d WHERE c.id=? AND d.id=?")?; for chat in all_chats { let (chat_id, typ, grpid, protected) = chat.context("Step 24")?; // In groups, this also contains past members @@ -1601,7 +1603,8 @@ fn migrate_pgp_contacts( let old_and_new_members = match typ { // 1:1 chats retain: - // - email-contact if peerstate is in the "reset" state. + // - email-contact if peerstate is in the "reset" state, + // or if there is no PGP-contact that has the right email address. // - PGP-contact identified by the Autocrypt key if Autocrypt key does not match the verified key. // - PGP-contact identified by the verified key if peerstate Autocrypt key matches the Verified key. // Since the autocrypt and verified PGP contact are identital in this case, we can add the AutocryptPgp contact, @@ -1615,6 +1618,12 @@ fn migrate_pgp_contacts( keep_email_contacts("No peerstate, or peerstate in 'reset' state"); continue; }; + if !addr_cmp_stmt + .query_row((old_member, new_contact), |row| row.get::<_, bool>(0))? + { + keep_email_contacts("PGP contact has different email"); + continue; + } vec![(*old_member, Some(new_contact))] } From 24e798a1d10ef9fc5f52b9f7dabe71b36ab0976c Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 6 May 2025 17:55:30 +0000 Subject: [PATCH 217/381] remove TODO --- src/sql/migrations.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 72ad692884..ee910a2fb2 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1259,8 +1259,6 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint); Ok((update_icons, disable_server_delete, recode_avatar)) } -// TODO if this takes very long, then it should be multiple transactions -// so that it can be executed in multiple steps fn migrate_pgp_contacts( context: &Context, transaction: &mut rusqlite::Transaction<'_>, From 0505cba3cf1364c336620ca4b00d2686aa1ef4b8 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 6 May 2025 18:13:41 +0000 Subject: [PATCH 218/381] unprotect 1:1 chats with email contacts --- src/sql/migrations.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index ee910a2fb2..b351e3b9ab 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1619,6 +1619,11 @@ fn migrate_pgp_contacts( if !addr_cmp_stmt .query_row((old_member, new_contact), |row| row.get::<_, bool>(0))? { + // Unprotect this 1:1 chat if it was protected. + // + // Otherwise we get protected chat with email-contact. + transaction.execute("UPDATE chats SET protected=0 WHERE id=?", (chat_id,))?; + keep_email_contacts("PGP contact has different email"); continue; } From 8fd4499b2f2a5f93b2287fb6fe8c0e48419419ea Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 6 May 2025 18:51:11 +0000 Subject: [PATCH 219/381] deal with NULL verifier in acpeerstates --- src/sql/migrations.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index b351e3b9ab..e44ed72f56 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1337,7 +1337,7 @@ fn migrate_pgp_contacts( "SELECT c.id, c.name, c.addr, c.origin, c.blocked, c.last_seen, c.authname, c.param, c.status, c.is_bot, c.selfavatar_sent, IFNULL(p.public_key, p.gossip_key), - p.verified_key, p.verifier, + p.verified_key, IFNULL(p.verifier, ''), p.secondary_verified_key, p.secondary_verifier, p.prefer_encrypted FROM contacts c INNER JOIN acpeerstates p ON c.addr=p.addr From ab94b884702fa0a1def8d3ef91fec4563ee48fdc Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 6 May 2025 20:05:44 +0000 Subject: [PATCH 220/381] mark device chat as encrypted --- src/chat.rs | 8 ++++++-- src/chat/chat_tests.rs | 12 ++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index 42f20093be..13f4bee631 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1919,8 +1919,12 @@ impl Chat { Chattype::Single => { let chat_contact_ids = get_chat_contacts(context, self.id).await?; if let Some(contact_id) = chat_contact_ids.first() { - let contact = Contact::get_by_id(context, *contact_id).await?; - contact.is_pgp_contact() + if *contact_id == ContactId::DEVICE { + true + } else { + let contact = Contact::get_by_id(context, *contact_id).await?; + contact.is_pgp_contact() + } } else { true } diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index bc18846d82..f25d14c7a8 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -991,6 +991,18 @@ async fn test_device_chat_cannot_sent() { assert!(forward_msgs(&t, &[msg_id], device_chat_id).await.is_err()); } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_device_chat_is_encrypted() { + let t = TestContext::new_alice().await; + t.update_device_chats().await.unwrap(); + let device_chat_id = ChatId::get_for_contact(&t, ContactId::DEVICE) + .await + .unwrap(); + + let device_chat = Chat::load_from_db(&t, device_chat_id).await.unwrap(); + assert!(device_chat.is_encrypted(&t).await.unwrap()); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_delete_and_reset_all_device_msgs() { let t = TestContext::new().await; From 303c6f6ab5f3aeed927e0fe7d7a1bc8c5cf89e0e Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 6 May 2025 20:46:32 +0000 Subject: [PATCH 221/381] hide non-pgp contacts from contact list --- src/contact.rs | 2 + src/contact/contact_tests.rs | 98 ++++++++++++---------------- src/receive_imf/receive_imf_tests.rs | 14 ++-- 3 files changed, 50 insertions(+), 64 deletions(-) diff --git a/src/contact.rs b/src/contact.rs index 88643a9c73..cd7897170a 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -1080,6 +1080,7 @@ impl Contact { "SELECT c.id, c.addr FROM contacts c LEFT JOIN acpeerstates ps ON c.addr=ps.addr \ WHERE c.id>? + AND c.fingerprint!='' \ AND c.origin>=? \ AND c.blocked=0 \ AND (iif(c.name='',c.authname,c.name) LIKE ? OR c.addr LIKE ?) \ @@ -1137,6 +1138,7 @@ impl Contact { .query_map( "SELECT id, addr FROM contacts WHERE id>? + AND fingerprint!='' AND origin>=? AND blocked=0 ORDER BY last_seen DESC, id DESC;", diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs index 05c82ffa26..2053dcda64 100644 --- a/src/contact/contact_tests.rs +++ b/src/contact/contact_tests.rs @@ -56,58 +56,45 @@ fn test_split_address_book() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_get_contacts() -> Result<()> { - let context = TestContext::new().await; - - assert!(context.get_all_self_addrs().await?.is_empty()); + let mut tcm = TestContextManager::new(); + let context = tcm.bob().await; + let alice = tcm.alice().await; + alice.set_config(Config::Displayname, Some("MyName")).await?; - // Bob is not in the contacts yet. - let contacts = Contact::get_all(&context.ctx, 0, Some("bob")).await?; + // Alice is not in the contacts yet. + let contacts = Contact::get_all(&context.ctx, 0, Some("Alice")).await?; assert_eq!(contacts.len(), 0); - let (id, _modified) = Contact::add_or_lookup( - &context.ctx, - "bob", - &ContactAddress::new("user@example.org")?, - Origin::IncomingReplyTo, - ) - .await?; + let id = context.add_or_lookup_contact_id(&alice).await; assert_ne!(id, ContactId::UNDEFINED); - let contact = Contact::get_by_id(&context.ctx, id).await.unwrap(); + let contact = Contact::get_by_id(&context, id).await.unwrap(); assert_eq!(contact.get_name(), ""); - assert_eq!(contact.get_authname(), "bob"); - assert_eq!(contact.get_display_name(), "bob"); + assert_eq!(contact.get_authname(), "MyName"); + assert_eq!(contact.get_display_name(), "MyName"); // Search by name. - let contacts = Contact::get_all(&context.ctx, 0, Some("bob")).await?; + let contacts = Contact::get_all(&context, 0, Some("myname")).await?; assert_eq!(contacts.len(), 1); assert_eq!(contacts.first(), Some(&id)); // Search by address. - let contacts = Contact::get_all(&context.ctx, 0, Some("user")).await?; + let contacts = Contact::get_all(&context, 0, Some("alice@example.org")).await?; assert_eq!(contacts.len(), 1); assert_eq!(contacts.first(), Some(&id)); - let contacts = Contact::get_all(&context.ctx, 0, Some("alice")).await?; + let contacts = Contact::get_all(&context, 0, Some("Foobar")).await?; assert_eq!(contacts.len(), 0); - // Set Bob name to "someone" manually. - let (contact_bob_id, modified) = Contact::add_or_lookup( - &context.ctx, - "someone", - &ContactAddress::new("user@example.org")?, - Origin::ManuallyCreated, - ) - .await?; - assert_eq!(contact_bob_id, id); - assert_eq!(modified, Modifier::Modified); + // Set Alice name to "someone" manually. + id.set_name(&context, "someone").await?; let contact = Contact::get_by_id(&context.ctx, id).await.unwrap(); assert_eq!(contact.get_name(), "someone"); - assert_eq!(contact.get_authname(), "bob"); + assert_eq!(contact.get_authname(), "MyName"); assert_eq!(contact.get_display_name(), "someone"); // Not searchable by authname, because it is not displayed. - let contacts = Contact::get_all(&context.ctx, 0, Some("bob")).await?; + let contacts = Contact::get_all(&context, 0, Some("MyName")).await?; assert_eq!(contacts.len(), 0); // Search by display name (same as manually set name). @@ -282,7 +269,7 @@ async fn test_contact_name_changes() -> Result<()> { assert_eq!(contact.get_display_name(), "f@example.org"); assert_eq!(contact.get_name_n_addr(), "f@example.org"); let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?; - assert_eq!(contacts.len(), 1); + assert_eq!(contacts.len(), 0); // second message inits the name receive_imf( @@ -308,9 +295,9 @@ async fn test_contact_name_changes() -> Result<()> { assert_eq!(contact.get_display_name(), "Flobbyfoo"); assert_eq!(contact.get_name_n_addr(), "Flobbyfoo (f@example.org)"); let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?; - assert_eq!(contacts.len(), 1); + assert_eq!(contacts.len(), 0); let contacts = Contact::get_all(&t, 0, Some("flobbyfoo")).await?; - assert_eq!(contacts.len(), 1); + assert_eq!(contacts.len(), 0); // third message changes the name receive_imf( @@ -338,11 +325,11 @@ async fn test_contact_name_changes() -> Result<()> { assert_eq!(contact.get_display_name(), "Foo Flobby"); assert_eq!(contact.get_name_n_addr(), "Foo Flobby (f@example.org)"); let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?; - assert_eq!(contacts.len(), 1); + assert_eq!(contacts.len(), 0); let contacts = Contact::get_all(&t, 0, Some("flobbyfoo")).await?; assert_eq!(contacts.len(), 0); let contacts = Contact::get_all(&t, 0, Some("Foo Flobby")).await?; - assert_eq!(contacts.len(), 1); + assert_eq!(contacts.len(), 0); // change name manually let test_id = Contact::create(&t, "Falk", "f@example.org").await?; @@ -356,9 +343,9 @@ async fn test_contact_name_changes() -> Result<()> { assert_eq!(contact.get_display_name(), "Falk"); assert_eq!(contact.get_name_n_addr(), "Falk (f@example.org)"); let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?; - assert_eq!(contacts.len(), 1); + assert_eq!(contacts.len(), 0); let contacts = Contact::get_all(&t, 0, Some("falk")).await?; - assert_eq!(contacts.len(), 1); + assert_eq!(contacts.len(), 0); Ok(()) } @@ -366,20 +353,13 @@ async fn test_contact_name_changes() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_delete() -> Result<()> { let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; assert!(Contact::delete(&alice, ContactId::SELF).await.is_err()); // Create Bob contact - let (contact_id, _) = Contact::add_or_lookup( - &alice, - "Bob", - &ContactAddress::new("bob@example.net")?, - Origin::ManuallyCreated, - ) - .await?; - let chat = alice - .create_chat_with_contact("Bob", "bob@example.net") - .await; + let contact_id = alice.add_or_lookup_contact_id(&bob).await; + let chat = alice.create_chat(&bob).await; assert_eq!( Contact::get_all(&alice, 0, Some("bob@example.net")) .await? @@ -416,30 +396,32 @@ async fn test_delete() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_delete_and_recreate_contact() -> Result<()> { - let t = TestContext::new_alice().await; + let mut tcm = TestContextManager::new(); + let t = tcm.alice().await; + let bob = tcm.bob().await; // test recreation after physical deletion - let contact_id1 = Contact::create(&t, "Foo", "foo@bar.de").await?; - assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1); + let contact_id1 = t.add_or_lookup_contact_id(&bob).await; + assert_eq!(Contact::get_all(&t, 0, Some("bob@example.net")).await?.len(), 1); Contact::delete(&t, contact_id1).await?; assert!(Contact::get_by_id(&t, contact_id1).await.is_err()); - assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 0); - let contact_id2 = Contact::create(&t, "Foo", "foo@bar.de").await?; + assert_eq!(Contact::get_all(&t, 0, Some("bob@example.net")).await?.len(), 0); + let contact_id2 = t.add_or_lookup_contact_id(&bob).await; assert_ne!(contact_id2, contact_id1); - assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1); + assert_eq!(Contact::get_all(&t, 0, Some("bob@example.net")).await?.len(), 1); // test recreation after hiding - t.create_chat_with_contact("Foo", "foo@bar.de").await; + t.create_chat(&bob).await; Contact::delete(&t, contact_id2).await?; let contact = Contact::get_by_id(&t, contact_id2).await?; assert_eq!(contact.origin, Origin::Hidden); - assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 0); + assert_eq!(Contact::get_all(&t, 0, Some("bob@example.net")).await?.len(), 0); - let contact_id3 = Contact::create(&t, "Foo", "foo@bar.de").await?; + let contact_id3 = t.add_or_lookup_contact_id(&bob).await; let contact = Contact::get_by_id(&t, contact_id3).await?; - assert_eq!(contact.origin, Origin::ManuallyCreated); + assert_eq!(contact.origin, Origin::CreateChat); assert_eq!(contact_id3, contact_id2); - assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1); + assert_eq!(Contact::get_all(&t, 0, Some("bob@example.net")).await?.len(), 1); Ok(()) } diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index bb9c57be86..cd46832462 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -3001,13 +3001,15 @@ async fn test_outgoing_private_reply_multidevice() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_auto_accept_for_bots() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config(Config::Bot, Some("1")).await.unwrap(); - receive_imf(&t, MSGRMSG, false).await?; - let msg = t.get_last_msg().await; - let chat = chat::Chat::load_from_db(&t, msg.chat_id).await?; + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + alice.set_config(Config::Bot, Some("1")).await.unwrap(); + let msg = tcm.send_recv(bob, alice, "Hello!").await; + let chat = chat::Chat::load_from_db(alice, msg.chat_id).await?; assert!(!chat.is_contact_request()); - assert!(Contact::get_all(&t, 0, None).await?.len() == 1); + + assert_eq!(Contact::get_all(alice, 0, None).await?.len(), 1); Ok(()) } From 443f75ee88a62b40b3d444b21020799bdce29236 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 6 May 2025 23:28:13 +0000 Subject: [PATCH 222/381] rustfmt --- src/contact/contact_tests.rs | 39 ++++++++++++++++++++++++++++++------ src/sql/migrations.rs | 3 ++- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs index 2053dcda64..fd4373dbc6 100644 --- a/src/contact/contact_tests.rs +++ b/src/contact/contact_tests.rs @@ -59,7 +59,9 @@ async fn test_get_contacts() -> Result<()> { let mut tcm = TestContextManager::new(); let context = tcm.bob().await; let alice = tcm.alice().await; - alice.set_config(Config::Displayname, Some("MyName")).await?; + alice + .set_config(Config::Displayname, Some("MyName")) + .await?; // Alice is not in the contacts yet. let contacts = Contact::get_all(&context.ctx, 0, Some("Alice")).await?; @@ -402,26 +404,51 @@ async fn test_delete_and_recreate_contact() -> Result<()> { // test recreation after physical deletion let contact_id1 = t.add_or_lookup_contact_id(&bob).await; - assert_eq!(Contact::get_all(&t, 0, Some("bob@example.net")).await?.len(), 1); + assert_eq!( + Contact::get_all(&t, 0, Some("bob@example.net")) + .await? + .len(), + 1 + ); Contact::delete(&t, contact_id1).await?; assert!(Contact::get_by_id(&t, contact_id1).await.is_err()); - assert_eq!(Contact::get_all(&t, 0, Some("bob@example.net")).await?.len(), 0); + assert_eq!( + Contact::get_all(&t, 0, Some("bob@example.net")) + .await? + .len(), + 0 + ); let contact_id2 = t.add_or_lookup_contact_id(&bob).await; assert_ne!(contact_id2, contact_id1); - assert_eq!(Contact::get_all(&t, 0, Some("bob@example.net")).await?.len(), 1); + assert_eq!( + Contact::get_all(&t, 0, Some("bob@example.net")) + .await? + .len(), + 1 + ); // test recreation after hiding t.create_chat(&bob).await; Contact::delete(&t, contact_id2).await?; let contact = Contact::get_by_id(&t, contact_id2).await?; assert_eq!(contact.origin, Origin::Hidden); - assert_eq!(Contact::get_all(&t, 0, Some("bob@example.net")).await?.len(), 0); + assert_eq!( + Contact::get_all(&t, 0, Some("bob@example.net")) + .await? + .len(), + 0 + ); let contact_id3 = t.add_or_lookup_contact_id(&bob).await; let contact = Contact::get_by_id(&t, contact_id3).await?; assert_eq!(contact.origin, Origin::CreateChat); assert_eq!(contact_id3, contact_id2); - assert_eq!(Contact::get_all(&t, 0, Some("bob@example.net")).await?.len(), 1); + assert_eq!( + Contact::get_all(&t, 0, Some("bob@example.net")) + .await? + .len(), + 1 + ); Ok(()) } diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index e44ed72f56..82c1750cc6 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1622,7 +1622,8 @@ fn migrate_pgp_contacts( // Unprotect this 1:1 chat if it was protected. // // Otherwise we get protected chat with email-contact. - transaction.execute("UPDATE chats SET protected=0 WHERE id=?", (chat_id,))?; + transaction + .execute("UPDATE chats SET protected=0 WHERE id=?", (chat_id,))?; keep_email_contacts("PGP contact has different email"); continue; From 806f067f1ed3548e4ee9182e06da6e0ad688424c Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 7 May 2025 12:27:52 +0000 Subject: [PATCH 223/381] fix python tests --- python/tests/test_1_online.py | 6 ++--- python/tests/test_3_offline.py | 43 +++++++++++++++++++--------------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/python/tests/test_1_online.py b/python/tests/test_1_online.py index 722d431fc9..564be7f236 100644 --- a/python/tests/test_1_online.py +++ b/python/tests/test_1_online.py @@ -1138,9 +1138,9 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp): lp.sec("create some chat content") some1_addr = some1.get_config("addr") - chat1 = ac1.create_contact(some1_addr, name="some1").create_chat() + chat1 = ac1.create_contact(some1).create_chat() chat1.send_text("msg1") - assert len(ac1.get_contacts(query="some1")) == 1 + assert len(ac1.get_contacts()) == 1 original_image_path = data.get_path("d.png") chat1.send_image(original_image_path) @@ -1152,7 +1152,7 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp): chat1.send_file(str(path)) def assert_account_is_proper(ac): - contacts = ac.get_contacts(query="some1") + contacts = ac.get_contacts() assert len(contacts) == 1 contact2 = contacts[0] assert contact2.addr == some1_addr diff --git a/python/tests/test_3_offline.py b/python/tests/test_3_offline.py index 7fbc25ccb3..673563ee34 100644 --- a/python/tests/test_3_offline.py +++ b/python/tests/test_3_offline.py @@ -137,15 +137,16 @@ def test_create_self_contact(self, acfactory): def test_get_contacts_and_delete(self, acfactory): ac1 = acfactory.get_pseudo_configured_account() - contact1 = ac1.create_contact("some1@example.org", name="some1") + ac2 = acfactory.get_pseudo_configured_account() + contact1 = ac1.create_contact(ac2) contacts = ac1.get_contacts() assert len(contacts) == 1 - assert contact1 in contacts assert not ac1.get_contacts(query="some2") - assert ac1.get_contacts(query="some1") + assert not ac1.get_contacts(query="some1") assert not ac1.get_contacts(only_verified=True) assert len(ac1.get_contacts(with_self=True)) == 2 + assert contact1 in ac1.get_contacts() assert ac1.delete_contact(contact1) assert contact1 not in ac1.get_contacts() @@ -439,7 +440,8 @@ def test_import_export_on_unencrypted_acct(self, acfactory, tmp_path): backupdir = tmp_path / "backup" backupdir.mkdir() ac1 = acfactory.get_pseudo_configured_account() - chat = ac1.create_contact("some1 ").create_chat() + ac_contact = acfactory.get_pseudo_configured_account() + chat = ac1.create_contact(ac_contact).create_chat() # send a text message msg = chat.send_text("msg1") # send a binary file @@ -454,10 +456,10 @@ def test_import_export_on_unencrypted_acct(self, acfactory, tmp_path): assert os.path.exists(path) ac2 = acfactory.get_unconfigured_account() ac2.import_all(path) - contacts = ac2.get_contacts(query="some1") + contacts = ac2.get_contacts() assert len(contacts) == 1 contact2 = contacts[0] - assert contact2.addr == "some1@example.org" + assert contact2.addr == ac_contact.get_config("addr") chat2 = contact2.create_chat() messages = chat2.get_messages() assert len(messages) == 2 @@ -470,8 +472,9 @@ def test_import_export_on_encrypted_acct(self, acfactory, tmp_path): backupdir = tmp_path / "backup" backupdir.mkdir() ac1 = acfactory.get_pseudo_configured_account(passphrase=passphrase1) + ac2 = acfactory.get_pseudo_configured_account() - chat = ac1.create_contact("some1 ").create_chat() + chat = ac1.create_contact(ac2).create_chat() # send a text message msg = chat.send_text("msg1") # send a binary file @@ -492,10 +495,10 @@ def test_import_export_on_encrypted_acct(self, acfactory, tmp_path): ac2.import_all(path) # check data integrity - contacts = ac2.get_contacts(query="some1") + contacts = ac2.get_contacts() assert len(contacts) == 1 contact2 = contacts[0] - assert contact2.addr == "some1@example.org" + contact2_addr = contact2.addr chat2 = contact2.create_chat() messages = chat2.get_messages() assert len(messages) == 2 @@ -509,10 +512,10 @@ def test_import_export_on_encrypted_acct(self, acfactory, tmp_path): ac2.open(passphrase2) # check data integrity - contacts = ac2.get_contacts(query="some1") + contacts = ac2.get_contacts() assert len(contacts) == 1 contact2 = contacts[0] - assert contact2.addr == "some1@example.org" + assert contact2.addr == contact2_addr chat2 = contact2.create_chat() messages = chat2.get_messages() assert len(messages) == 2 @@ -525,8 +528,9 @@ def test_import_export_with_passphrase(self, acfactory, tmp_path): backupdir = tmp_path / "backup" backupdir.mkdir() ac1 = acfactory.get_pseudo_configured_account() + ac_contact = acfactory.get_pseudo_configured_account() - chat = ac1.create_contact("some1 ").create_chat() + chat = ac1.create_contact(ac_contact).create_chat() # send a text message msg = chat.send_text("msg1") # send a binary file @@ -548,10 +552,10 @@ def test_import_export_with_passphrase(self, acfactory, tmp_path): ac2.import_all(path, passphrase) # check data integrity - contacts = ac2.get_contacts(query="some1") + contacts = ac2.get_contacts() assert len(contacts) == 1 contact2 = contacts[0] - assert contact2.addr == "some1@example.org" + assert contact2.addr == ac_contact.get_config("addr") chat2 = contact2.create_chat() messages = chat2.get_messages() assert len(messages) == 2 @@ -570,7 +574,8 @@ def test_import_encrypted_bak_into_encrypted_acct(self, acfactory, tmp_path): backupdir.mkdir() ac1 = acfactory.get_pseudo_configured_account() - chat = ac1.create_contact("some1 ").create_chat() + ac_contact = acfactory.get_pseudo_configured_account() + chat = ac1.create_contact(ac_contact).create_chat() # send a text message msg = chat.send_text("msg1") # send a binary file @@ -593,10 +598,10 @@ def test_import_encrypted_bak_into_encrypted_acct(self, acfactory, tmp_path): ac2.import_all(path, bak_passphrase) # check data integrity - contacts = ac2.get_contacts(query="some1") + contacts = ac2.get_contacts() assert len(contacts) == 1 contact2 = contacts[0] - assert contact2.addr == "some1@example.org" + assert contact2.addr == ac_contact.get_config("addr") chat2 = contact2.create_chat() messages = chat2.get_messages() assert len(messages) == 2 @@ -610,10 +615,10 @@ def test_import_encrypted_bak_into_encrypted_acct(self, acfactory, tmp_path): ac2.open(acct_passphrase) # check data integrity - contacts = ac2.get_contacts(query="some1") + contacts = ac2.get_contacts() assert len(contacts) == 1 contact2 = contacts[0] - assert contact2.addr == "some1@example.org" + assert contact2.addr == ac_contact.get_config("addr") chat2 = contact2.create_chat() messages = chat2.get_messages() assert len(messages) == 2 From 9891ca71e66810c572bfb8a08fbbe4fa25b4c4c9 Mon Sep 17 00:00:00 2001 From: l Date: Wed, 7 May 2025 15:59:38 +0000 Subject: [PATCH 224/381] test fixup Co-authored-by: Hocuri --- deltachat-rpc-client/tests/test_securejoin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deltachat-rpc-client/tests/test_securejoin.py b/deltachat-rpc-client/tests/test_securejoin.py index 0a49a7ad88..27008c7514 100644 --- a/deltachat-rpc-client/tests/test_securejoin.py +++ b/deltachat-rpc-client/tests/test_securejoin.py @@ -22,7 +22,7 @@ def test_qr_setup_contact(acfactory, tmp_path) -> None: bob.wait_for_securejoin_joiner_success() # Test that Bob verified Alice's profile. - bob_contact_alice = bob.create_contact(bob) + bob_contact_alice = bob.create_contact(alice) bob_contact_alice_snapshot = bob_contact_alice.get_snapshot() assert bob_contact_alice_snapshot.is_verified From b82c054f3e22c505e4b449b01376cf502b6e2679 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Thu, 8 May 2025 14:49:00 +0200 Subject: [PATCH 225/381] Add doc comment for get_migration_error() --- deltachat-jsonrpc/src/api.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/deltachat-jsonrpc/src/api.rs b/deltachat-jsonrpc/src/api.rs index 5919a09ff2..f23883ee75 100644 --- a/deltachat-jsonrpc/src/api.rs +++ b/deltachat-jsonrpc/src/api.rs @@ -354,6 +354,15 @@ impl CommandApi { Ok(ctx.get_blobdir().to_str().map(|s| s.to_owned())) } + /// If there was an error while the account was opened + /// and migrated to the current version, + /// then this function returns it. + /// + /// This function is useful because the PGP-contacts migration could fail due to bugs + /// and then the account will not work properly. + /// + /// After opening an account, the UI should call this function + /// and show the error string if one is returned. async fn get_migration_error(&self, account_id: u32) -> Result> { let ctx = self.get_context(account_id).await?; Ok(ctx.get_migration_error()) From cf8a30cd18b40b0cf558a1ba60c35f63911fb419 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 7 May 2025 17:57:46 +0000 Subject: [PATCH 226/381] typo fix --- deltachat-jsonrpc/src/api/types/contact.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deltachat-jsonrpc/src/api/types/contact.rs b/deltachat-jsonrpc/src/api/types/contact.rs index 513ad39e75..5c367012ff 100644 --- a/deltachat-jsonrpc/src/api/types/contact.rs +++ b/deltachat-jsonrpc/src/api/types/contact.rs @@ -25,7 +25,7 @@ pub struct ContactObject { /// Is encryption available for this contact. /// - /// This can only be true of PGP-contacts. + /// This can only be true for PGP-contacts. /// However, it is possible to have a PGP-contact /// for which encryption is not available because we don't have a key yet, /// e.g. if we just scanned the fingerprint from a QR code. From 4050d8d5edb0488215b75f4b47514aad3ac477c1 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 7 May 2025 18:04:56 +0000 Subject: [PATCH 227/381] typo fix --- deltachat-rpc-client/tests/test_securejoin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deltachat-rpc-client/tests/test_securejoin.py b/deltachat-rpc-client/tests/test_securejoin.py index 27008c7514..754a715cac 100644 --- a/deltachat-rpc-client/tests/test_securejoin.py +++ b/deltachat-rpc-client/tests/test_securejoin.py @@ -213,7 +213,7 @@ def test_setup_contact_resetup(acfactory) -> None: def test_verified_group_member_added_recovery(acfactory) -> None: - """Tests verified group recovery by reverifying than removing and adding a member back.""" + """Tests verified group recovery by reverifying then removing and adding a member back.""" ac1, ac2, ac3 = acfactory.get_online_accounts(3) logging.info("ac1 creates verified group") From 078fde08bdf930d659afc5e3fa37a59aa27a2ee3 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 7 May 2025 19:47:57 +0000 Subject: [PATCH 228/381] remove dots from errors --- src/contact.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/contact.rs b/src/contact.rs index cd7897170a..2a067e0392 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -1888,7 +1888,7 @@ pub(crate) async fn mark_contact_id_as_verified( |row| row.get(0), )?; if contact_fingerprint.is_empty() { - bail!("Non-PGP contact {contact_id} cannot be verified."); + bail!("Non-PGP contact {contact_id} cannot be verified"); } if verifier_id != ContactId::SELF { let verifier_fingerprint: String = transaction.query_row( @@ -1898,7 +1898,7 @@ pub(crate) async fn mark_contact_id_as_verified( )?; if verifier_fingerprint.is_empty() { bail!( - "Contact {contact_id} cannot be verified by non-PGP contact {verifier_id}." + "Contact {contact_id} cannot be verified by non-PGP contact {verifier_id}" ); } } From 8c6d12b859e5aabb7d47d56871ccdbc3204c4d39 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 7 May 2025 20:34:40 +0000 Subject: [PATCH 229/381] failing test for chat assignment --- src/chat/chat_tests.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index f25d14c7a8..c5c48330b5 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -4117,6 +4117,20 @@ async fn test_oneone_gossip() -> Result<()> { assert_eq!(rcvd_msg2.get_showpadlock(), true); assert_eq!(rcvd_msg2.text, "Hello from second device!"); + tcm.section("Alice sends another message from the first devicer"); + let sent_msg3 = alice.send_text(alice_chat.id, "Hello again, Bob!").await; + + // This message has no Autocrypt-Gossip header, + // but should still be assigned to PGP-contact. + tcm.section("Alice receives a copy of another message on second device"); + let rcvd_msg3 = alice2.recv_msg(&sent_msg3).await; + assert_eq!(rcvd_msg3.get_showpadlock(), true); + assert_eq!(rcvd_msg3.chat_id, rcvd_msg.chat_id); + + // Check that there was no gossip. + let parsed_msg3 = alice2.parse_msg(&sent_msg3).await; + assert!(!parsed_msg3.header_exists(HeaderDef::AutocryptGossip)); + Ok(()) } From 6cb4d4321089935a628ecdb39660c0b2ae7a7983 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 8 May 2025 17:08:08 +0000 Subject: [PATCH 230/381] assign outgoing chat messages by references --- src/receive_imf.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 090a7eefa5..bb3dfbdc88 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -330,6 +330,19 @@ pub(crate) async fn receive_imf_inner( } else { None } + } else if is_partial_download.is_none() && !mime_parser.incoming { + if let Some(parent) = get_parent_message( + context, + mime_parser.get_header(HeaderDef::References), + mime_parser.get_header(HeaderDef::InReplyTo), + ) + .await? + { + info!(context, "Found parent chat {}", parent.chat_id); + Some(parent.chat_id) + } else { + None + } } else { None }; @@ -400,6 +413,14 @@ pub(crate) async fn receive_imf_inner( // mapped it to a PGP contact. // This is a 1:1 PGP-chat. to_ids = pgp_to_ids + } else if let Some(chat_id) = chat_id { + to_ids = lookup_pgp_contacts_by_address_list( + context, + &mime_parser.recipients, + to_member_fingerprints, + chat_id, + ) + .await?; } else { to_ids = add_or_lookup_contacts_by_address_list( context, From eef17f9403c65422db70364627624b399a3aa3d3 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 9 May 2025 20:31:58 +0000 Subject: [PATCH 231/381] comment why we dont assign the message to chat before recipient lookup --- src/receive_imf.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index bb3dfbdc88..a542b5c26c 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -322,6 +322,12 @@ pub(crate) async fn receive_imf_inner( } }; + // ID of the chat to look up the addresses in. + // + // Note that this is not necessarily the chat we want to assign the message to. + // In case of an outgoing private reply to a group message we may + // lookup the address of receipient in the list of addresses used in the group, + // but want to assign the message to 1:1 chat. let chat_id = if let Some(grpid) = mime_parser.get_chat_group_id() { if let Some((chat_id, _protected, _blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? @@ -338,7 +344,6 @@ pub(crate) async fn receive_imf_inner( ) .await? { - info!(context, "Found parent chat {}", parent.chat_id); Some(parent.chat_id) } else { None From 501010a17f66bb10a93a0b2c7898d30ed2ca1749 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 9 May 2025 20:39:33 +0000 Subject: [PATCH 232/381] remove TODO --- src/receive_imf.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index a542b5c26c..bfb218fc3d 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -3126,7 +3126,6 @@ async fn has_verified_encryption( }; if from_id == ContactId::SELF { - // TODO: check that the message is signed with our key? return Ok(Verified); } From 3e8e540f1ec030e55f3d4afb09d0efe399ff1539 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 9 May 2025 20:41:12 +0000 Subject: [PATCH 233/381] remove FIXME --- src/chat.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat.rs b/src/chat.rs index 13f4bee631..a676f231ec 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -3949,7 +3949,7 @@ pub(crate) async fn add_contact_to_chat_ex( if chat.typ == Chattype::Group && chat.is_promoted() { msg.viewtype = Viewtype::Text; - let contact_addr = contact.get_addr().to_lowercase(); // FIXME contact is not identified by addr + let contact_addr = contact.get_addr().to_lowercase(); msg.text = stock_str::msg_add_member_local(context, contact.id, ContactId::SELF).await; msg.param.set_cmd(SystemMessage::MemberAddedToGroup); msg.param.set(Param::Arg, contact_addr); From 463f52491c868c60838b96b159392427d4830ddb Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 10 May 2025 18:52:21 +0000 Subject: [PATCH 234/381] missingkey --- src/chat.rs | 35 ++++++++++-------- src/securejoin/securejoin_tests.rs | 57 ++++++++++-------------------- 2 files changed, 39 insertions(+), 53 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index a676f231ec..d3ad61065c 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -128,8 +128,8 @@ pub(crate) enum CantSendReason { /// Not a member of the chat. NotAMember, - /// Temporary state for 1:1 chats while SecureJoin is in progress. - SecurejoinWait, + /// State for 1:1 chat with a PGP-contact that does not have a key. + MissingKey, } impl fmt::Display for CantSendReason { @@ -149,7 +149,7 @@ impl fmt::Display for CantSendReason { write!(f, "mailing list does not have a know post address") } Self::NotAMember => write!(f, "not a member of the chat"), - Self::SecurejoinWait => write!(f, "awaiting SecureJoin for 1:1 chat"), + Self::MissingKey => write!(f, "OpenPGP key is missing"), } } } @@ -1673,15 +1673,20 @@ impl Chat { if !skip_fn(&reason) && !self.is_self_in_chat(context).await? { return Ok(Some(reason)); } - let reason = SecurejoinWait; - if !skip_fn(&reason) - && self - .check_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT) - .await? - > 0 - { - return Ok(Some(reason)); + + let reason = MissingKey; + if !skip_fn(&reason) && self.typ == Chattype::Single { + let contact_ids = get_chat_contacts(context, self.id).await?; + if let Some(contact_id) = contact_ids.first() { + let contact = Contact::get_by_id(context, *contact_id).await?; + if contact.is_pgp_contact() { + if contact.public_key(context).await?.is_none() { + return Ok(Some(reason)); + } + } + } } + Ok(None) } @@ -1695,7 +1700,6 @@ impl Chat { /// Returns the remaining timeout for the 1:1 chat in-progress SecureJoin. /// /// If the timeout has expired, adds an info message with additional information. - /// See also [`CantSendReason::SecurejoinWait`]. pub(crate) async fn check_securejoin_wait( &self, context: &Context, @@ -2945,8 +2949,7 @@ async fn prepare_send_msg( let skip_fn = |reason: &CantSendReason| match reason { CantSendReason::ProtectionBroken - | CantSendReason::ContactRequest - | CantSendReason::SecurejoinWait => { + | CantSendReason::ContactRequest => { // Allow securejoin messages, they are supposed to repair the verification. // If the chat is a contact request, let the user accept it later. msg.param.get_cmd() == SystemMessage::SecurejoinMessage @@ -2955,6 +2958,10 @@ async fn prepare_send_msg( // Necessary checks should be made anyway before removing contact // from the chat. CantSendReason::NotAMember => msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup, + CantSendReason::MissingKey => msg + .param + .get_bool(Param::ForcePlaintext) + .unwrap_or_default(), _ => false, }; if let Some(reason) = chat.why_cant_send_ex(context, &skip_fn).await? { diff --git a/src/securejoin/securejoin_tests.rs b/src/securejoin/securejoin_tests.rs index 97b3dd362b..619db08171 100644 --- a/src/securejoin/securejoin_tests.rs +++ b/src/securejoin/securejoin_tests.rs @@ -17,7 +17,6 @@ enum SetupContactCase { Normal, CheckProtectionTimestamp, WrongAliceGossip, - SecurejoinWaitTimeout, AliceIsBot, AliceHasName, } @@ -37,11 +36,6 @@ async fn test_setup_contact_wrong_alice_gossip() { test_setup_contact_ex(SetupContactCase::WrongAliceGossip).await } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_setup_contact_wait_timeout() { - test_setup_contact_ex(SetupContactCase::SecurejoinWaitTimeout).await -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_setup_contact_alice_is_bot() { test_setup_contact_ex(SetupContactCase::AliceIsBot).await @@ -103,11 +97,16 @@ async fn test_setup_contact_ex(case: SetupContactCase) { .unwrap(); tcm.section("Step 2: Bob scans QR-code, sends vc-request"); - join_securejoin(&bob.ctx, &qr).await.unwrap(); + let bob_chat_id = join_securejoin(&bob.ctx, &qr).await.unwrap(); assert_eq!( Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(), 1 ); + let bob_chat = Chat::load_from_db(&bob, bob_chat_id).await.unwrap(); + assert_eq!( + bob_chat.why_cant_send(&bob).await.unwrap(), + Some(CantSendReason::MissingKey) + ); let contact_alice_id = bob.add_or_lookup_pgp_contact(&alice).await.id; let sent = bob.pop_sent_msg().await; assert!(!sent.payload.contains("Bob Examplenet")); @@ -137,16 +136,9 @@ async fn test_setup_contact_ex(case: SetupContactCase) { msg.get_header(HeaderDef::SecureJoin).unwrap(), "vc-auth-required" ); + let bob_chat = bob.get_pgp_chat(&alice).await; - assert_eq!(bob_chat.can_send(&bob).await.unwrap(), false); - assert_eq!( - bob_chat.why_cant_send(&bob).await.unwrap(), - Some(CantSendReason::SecurejoinWait) - ); - if case == SetupContactCase::SecurejoinWaitTimeout { - SystemTime::shift(Duration::from_secs(constants::SECUREJOIN_WAIT_TIMEOUT)); - assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true); - } + assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true); tcm.section("Step 4: Bob receives vc-auth-required, sends vc-request-with-auth"); bob.recv_msg_trash(&sent).await; @@ -298,35 +290,22 @@ async fn test_setup_contact_ex(case: SetupContactCase) { assert!(contact_alice.get_name().is_empty()); assert_eq!(contact_alice.is_bot(), case == SetupContactCase::AliceIsBot); - if case != SetupContactCase::SecurejoinWaitTimeout { - // Later we check that the timeout message isn't added to the already protected chat. - SystemTime::shift(Duration::from_secs(constants::SECUREJOIN_WAIT_TIMEOUT + 1)); - assert_eq!( - bob_chat - .check_securejoin_wait(&bob, constants::SECUREJOIN_WAIT_TIMEOUT) - .await - .unwrap(), - 0 - ); - } + // Later we check that the timeout message isn't added to the already protected chat. + SystemTime::shift(Duration::from_secs(constants::SECUREJOIN_WAIT_TIMEOUT + 1)); + assert_eq!( + bob_chat + .check_securejoin_wait(&bob, constants::SECUREJOIN_WAIT_TIMEOUT) + .await + .unwrap(), + 0 + ); // Check Bob got expected info messages in his 1:1 chat. - let msg_cnt: usize = match case { - SetupContactCase::SecurejoinWaitTimeout => 3, - _ => 2, - }; + let msg_cnt = 2; let mut i = 0..msg_cnt; let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await; assert!(msg.is_info()); assert_eq!(msg.get_text(), stock_str::securejoin_wait(&bob).await); - if case == SetupContactCase::SecurejoinWaitTimeout { - let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await; - assert!(msg.is_info()); - assert_eq!( - msg.get_text(), - stock_str::securejoin_takes_longer(&bob).await - ); - } let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await; assert!(msg.is_info()); assert_eq!(msg.get_text(), chat_protection_enabled(&bob).await); From 2f55190e3ddad6c73a39992ca1760465b5ab0432 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 10 May 2025 20:22:42 +0000 Subject: [PATCH 235/381] remove securejoin timeout --- src/chat.rs | 116 +---------------------------- src/constants.rs | 5 -- src/securejoin/bob.rs | 3 +- src/securejoin/securejoin_tests.rs | 12 +-- src/sql.rs | 6 +- src/stock_str.rs | 5 -- 6 files changed, 6 insertions(+), 141 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index d3ad61065c..aea5c313ef 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -15,16 +15,14 @@ use deltachat_derive::{FromSql, ToSql}; use mail_builder::mime::MimePart; use serde::{Deserialize, Serialize}; use strum_macros::EnumIter; -use tokio::task; use crate::blob::BlobObject; use crate::chatlist::Chatlist; use crate::color::str_to_color; use crate::config::Config; use crate::constants::{ - self, Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, - DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS, EDITED_PREFIX, - TIMESTAMP_SENT_TOLERANCE, + Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CHAT_ID_LAST_SPECIAL, + DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS, EDITED_PREFIX, TIMESTAMP_SENT_TOLERANCE, }; use crate::contact::{self, Contact, ContactId, Origin}; use crate::context::Context; @@ -1450,18 +1448,6 @@ impl ChatId { Ok(sort_timestamp) } - - /// Spawns a task checking after a timeout whether the SecureJoin has finished for the 1:1 chat - /// and otherwise notifying the user accordingly. - pub(crate) fn spawn_securejoin_wait(self, context: &Context, timeout: u64) { - let context = context.clone(); - task::spawn(async move { - tokio::time::sleep(Duration::from_secs(timeout)).await; - let chat = Chat::load_from_db(&context, self).await?; - chat.check_securejoin_wait(&context, 0).await?; - Result::<()>::Ok(()) - }); - } } impl std::fmt::Display for ChatId { @@ -1697,73 +1683,6 @@ impl Chat { Ok(self.why_cant_send(context).await?.is_none()) } - /// Returns the remaining timeout for the 1:1 chat in-progress SecureJoin. - /// - /// If the timeout has expired, adds an info message with additional information. - pub(crate) async fn check_securejoin_wait( - &self, - context: &Context, - timeout: u64, - ) -> Result { - if self.typ != Chattype::Single || self.protected != ProtectionStatus::Unprotected { - return Ok(0); - } - - // chat is single and unprotected: - // get last info message of type SecurejoinWait or SecurejoinWaitTimeout - let (mut param_wait, mut param_timeout) = (Params::new(), Params::new()); - param_wait.set_cmd(SystemMessage::SecurejoinWait); - param_timeout.set_cmd(SystemMessage::SecurejoinWaitTimeout); - let (param_wait, param_timeout) = (param_wait.to_string(), param_timeout.to_string()); - let Some((param, ts_sort, ts_start)) = context - .sql - .query_row_optional( - "SELECT param, timestamp, timestamp_sent FROM msgs WHERE id=\ - (SELECT MAX(id) FROM msgs WHERE chat_id=? AND param IN (?, ?))", - (self.id, ¶m_wait, ¶m_timeout), - |row| { - let param: String = row.get(0)?; - let ts_sort: i64 = row.get(1)?; - let ts_start: i64 = row.get(2)?; - Ok((param, ts_sort, ts_start)) - }, - ) - .await? - else { - return Ok(0); - }; - if param == param_timeout { - return Ok(0); - } - - let now = time(); - // Don't await SecureJoin if the clock was set back. - if ts_start <= now { - let timeout = ts_start - .saturating_add(timeout.try_into()?) - .saturating_sub(now); - if timeout > 0 { - return Ok(timeout as u64); - } - } - add_info_msg_with_cmd( - context, - self.id, - &stock_str::securejoin_takes_longer(context).await, - SystemMessage::SecurejoinWaitTimeout, - // Use the sort timestamp of the "please wait" message, this way the added message is - // never sorted below the protection message if the SecureJoin finishes in parallel. - ts_sort, - Some(now), - None, - None, - None, - ) - .await?; - context.emit_event(EventType::ChatModified(self.id)); - Ok(0) - } - /// Checks if the user is part of a chat /// and has basically the permissions to edit the chat therefore. /// The function does not check if the chat type allows editing of concrete elements. @@ -2600,34 +2519,6 @@ pub(crate) async fn update_special_chat_names(context: &Context) -> Result<()> { Ok(()) } -/// Checks if there is a 1:1 chat in-progress SecureJoin for Bob and, if necessary, schedules a task -/// unblocking the chat and notifying the user accordingly. -pub(crate) async fn resume_securejoin_wait(context: &Context) -> Result<()> { - let chat_ids: Vec = context - .sql - .query_map( - "SELECT chat_id FROM bobstate", - (), - |row| { - let chat_id: ChatId = row.get(0)?; - Ok(chat_id) - }, - |rows| rows.collect::, _>>().map_err(Into::into), - ) - .await?; - - for chat_id in chat_ids { - let chat = Chat::load_from_db(context, chat_id).await?; - let timeout = chat - .check_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT) - .await?; - if timeout > 0 { - chat_id.spawn_securejoin_wait(context, timeout); - } - } - Ok(()) -} - /// Handle a [`ChatId`] and its [`Blocked`] status at once. /// /// This struct is an optimisation to read a [`ChatId`] and its [`Blocked`] status at once @@ -2948,8 +2839,7 @@ async fn prepare_send_msg( let mut chat = Chat::load_from_db(context, chat_id).await?; let skip_fn = |reason: &CantSendReason| match reason { - CantSendReason::ProtectionBroken - | CantSendReason::ContactRequest => { + CantSendReason::ProtectionBroken | CantSendReason::ContactRequest => { // Allow securejoin messages, they are supposed to repair the verification. // If the chat is a contact request, let the user accept it later. msg.param.get_cmd() == SystemMessage::SecurejoinMessage diff --git a/src/constants.rs b/src/constants.rs index d4a1e326f3..a02b3b86d9 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -211,11 +211,6 @@ pub(crate) const DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT: u64 = 12 * 60 * 60; /// in the group membership consistency algo to reject outdated membership changes. pub(crate) const TIMESTAMP_SENT_TOLERANCE: i64 = 60; -/// How long a 1:1 chat can't be used for sending while the SecureJoin is in progress. This should -/// be 10-20 seconds so that we are reasonably sure that the app remains active and receiving also -/// on mobile devices. See also [`crate::chat::CantSendReason::SecurejoinWait`]. -pub(crate) const SECUREJOIN_WAIT_TIMEOUT: u64 = 15; - // To make text edits clearer for Non-Delta-MUA or old Delta Chats, edited text will be prefixed by EDITED_PREFIX. // Newer Delta Chats will remove the prefix as needed. pub(crate) const EDITED_PREFIX: &str = "✏️"; diff --git a/src/securejoin/bob.rs b/src/securejoin/bob.rs index 0a0c5f3543..fb3a840f9a 100644 --- a/src/securejoin/bob.rs +++ b/src/securejoin/bob.rs @@ -5,7 +5,7 @@ use anyhow::{Context as _, Result}; use super::qrinvite::QrInvite; use super::HandshakeMessage; use crate::chat::{self, is_contact_in_chat, ChatId, ProtectionStatus}; -use crate::constants::{self, Blocked, Chattype}; +use crate::constants::{Blocked, Chattype}; use crate::contact::Origin; use crate::context::Context; use crate::events::EventType; @@ -136,7 +136,6 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul None, ) .await?; - chat_id.spawn_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT); } Ok(chat_id) } diff --git a/src/securejoin/securejoin_tests.rs b/src/securejoin/securejoin_tests.rs index 619db08171..34be45580f 100644 --- a/src/securejoin/securejoin_tests.rs +++ b/src/securejoin/securejoin_tests.rs @@ -3,7 +3,7 @@ use deltachat_contact_tools::EmailAddress; use super::*; use crate::chat::{remove_contact_from_chat, CantSendReason}; use crate::chatlist::Chatlist; -use crate::constants::{self, Chattype}; +use crate::constants::Chattype; use crate::receive_imf::receive_imf; use crate::stock_str::{self, chat_protection_enabled}; use crate::test_utils::{ @@ -290,16 +290,6 @@ async fn test_setup_contact_ex(case: SetupContactCase) { assert!(contact_alice.get_name().is_empty()); assert_eq!(contact_alice.is_bot(), case == SetupContactCase::AliceIsBot); - // Later we check that the timeout message isn't added to the already protected chat. - SystemTime::shift(Duration::from_secs(constants::SECUREJOIN_WAIT_TIMEOUT + 1)); - assert_eq!( - bob_chat - .check_securejoin_wait(&bob, constants::SECUREJOIN_WAIT_TIMEOUT) - .await - .unwrap(), - 0 - ); - // Check Bob got expected info messages in his 1:1 chat. let msg_cnt = 2; let mut i = 0..msg_cnt; diff --git a/src/sql.rs b/src/sql.rs index 6c46dbab4a..93f37e0a90 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -8,7 +8,7 @@ use rusqlite::{config::DbConfig, types::ValueRef, Connection, OpenFlags, Row}; use tokio::sync::RwLock; use crate::blob::BlobObject; -use crate::chat::{self, add_device_msg, update_device_icon, update_saved_messages_icon}; +use crate::chat::{add_device_msg, update_device_icon, update_saved_messages_icon}; use crate::config::Config; use crate::constants::DC_CHAT_ID_TRASH; use crate::context::Context; @@ -286,10 +286,6 @@ impl Sql { { set_debug_logging_xdc(context, Some(MsgId::new(xdc_id))).await?; } - chat::resume_securejoin_wait(context) - .await - .log_err(context) - .ok(); Ok(()) } diff --git a/src/stock_str.rs b/src/stock_str.rs index fc5313dd63..255e05b595 100644 --- a/src/stock_str.rs +++ b/src/stock_str.rs @@ -805,11 +805,6 @@ pub(crate) async fn securejoin_wait(context: &Context) -> String { translated(context, StockMessage::SecurejoinWait).await } -/// Stock string: `The contact must be online to proceed. This process will continue automatically in background.`. -pub(crate) async fn securejoin_takes_longer(context: &Context) -> String { - translated(context, StockMessage::SecurejoinTakesLonger).await -} - /// Stock string: `Scan to chat with %1$s`. pub(crate) async fn setup_contact_qr_description( context: &Context, From 1205d09699b9181171327c4fcdb6518646b1ebf2 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 10 May 2025 20:39:09 +0000 Subject: [PATCH 236/381] clippy --- src/chat.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index aea5c313ef..0a0b49adf6 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1665,10 +1665,8 @@ impl Chat { let contact_ids = get_chat_contacts(context, self.id).await?; if let Some(contact_id) = contact_ids.first() { let contact = Contact::get_by_id(context, *contact_id).await?; - if contact.is_pgp_contact() { - if contact.public_key(context).await?.is_none() { - return Ok(Some(reason)); - } + if contact.is_pgp_contact() && contact.public_key(context).await?.is_none() { + return Ok(Some(reason)); } } } From 46752cd4d8776f26c520380be81daff84a5cfa7f Mon Sep 17 00:00:00 2001 From: Hocuri Date: Sun, 11 May 2025 20:18:55 +0200 Subject: [PATCH 237/381] Remove peerstate references --- deltachat-repl/src/cmdline.rs | 5 ++++- src/contact.rs | 3 +-- src/contact/contact_tests.rs | 2 +- src/context.rs | 2 +- src/mimeparser.rs | 21 ++++++++++----------- src/test_utils.rs | 7 +++---- src/tests/verified_chats.rs | 13 ++++++++++--- 7 files changed, 30 insertions(+), 23 deletions(-) diff --git a/deltachat-repl/src/cmdline.rs b/deltachat-repl/src/cmdline.rs index d00cf65708..65e87672f2 100644 --- a/deltachat-repl/src/cmdline.rs +++ b/deltachat-repl/src/cmdline.rs @@ -496,7 +496,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed"); } "reset" => { - ensure!(!arg1.is_empty(), "Argument missing: 1=jobs, 2=peerstates, 4=private keys, 8=rest but server config"); + ensure!( + !arg1.is_empty(), + "Argument missing: 4=private keys, 8=rest but server config" + ); let bits: i32 = arg1.parse()?; ensure!(bits < 16, " must be lower than 16."); reset_tables(&context, bits).await; diff --git a/src/contact.rs b/src/contact.rs index 2a067e0392..5804a7a828 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -1078,13 +1078,12 @@ impl Contact { .sql .query_map( "SELECT c.id, c.addr FROM contacts c - LEFT JOIN acpeerstates ps ON c.addr=ps.addr \ WHERE c.id>? AND c.fingerprint!='' \ AND c.origin>=? \ AND c.blocked=0 \ AND (iif(c.name='',c.authname,c.name) LIKE ? OR c.addr LIKE ?) \ - AND (1=? OR LENGTH(ps.verified_key_fingerprint)!=0) \ + AND (1=? OR c.verifier!=0) \ ORDER BY c.last_seen DESC, c.id DESC;", ( ContactId::LAST_SPECIAL, diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs index fd4373dbc6..86b412bec2 100644 --- a/src/contact/contact_tests.rs +++ b/src/contact/contact_tests.rs @@ -988,7 +988,7 @@ async fn test_verified_by_none() -> Result<()> { let contact = Contact::get_by_id(&alice, contact_id).await?; assert!(contact.get_verifier_id(&alice).await?.is_none()); - // Receive a message from Bob to create a peerstate. + // Receive a message from Bob to save the public key. let chat = bob.create_chat(&alice).await; let sent_msg = bob.send_text(chat.id, "moin").await; alice.recv_msg(&sent_msg).await; diff --git a/src/context.rs b/src/context.rs index a1288d2240..6a3ed5e2ad 100644 --- a/src/context.rs +++ b/src/context.rs @@ -811,7 +811,7 @@ impl Context { let pub_key_cnt = self .sql - .count("SELECT COUNT(*) FROM acpeerstates;", ()) + .count("SELECT COUNT(*) FROM public_keys;", ()) .await?; let fingerprint_str = match load_self_public_key(self).await { Ok(key) => key.dc_fingerprint().hex(), diff --git a/src/mimeparser.rs b/src/mimeparser.rs index bf34e13456..248ba969b3 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -82,9 +82,9 @@ pub(crate) struct MimeMessage { /// If a message is not encrypted or the signature is not valid, /// this set is empty. pub signatures: HashSet, - /// The mail recipient addresses for which gossip headers were applied - /// and their respective gossiped keys, - /// regardless of whether they modified any peerstates. + + /// The addresses for which there was a gossip header + /// and their respective gossiped keys. pub gossiped_keys: HashMap, /// Fingerprint of the key in the Autocrypt header. @@ -493,8 +493,7 @@ impl MimeMessage { // encryption here, but let's follow the standard. let gossip_headers = mail.headers.get_all_values("Autocrypt-Gossip"); gossiped_keys = - update_gossip_peerstates(context, &from.addr, &recipients, gossip_headers) - .await?; + parse_gossip_headers(context, &from.addr, &recipients, gossip_headers).await?; } if let Some(inner_from) = inner_from { @@ -1892,12 +1891,12 @@ fn remove_header( } } -/// Parses `Autocrypt-Gossip` headers from the email and applies them to peerstates. -/// Params: -/// from: The address which sent the message currently being parsed +/// Parses `Autocrypt-Gossip` headers from the email, +/// saves the keys into the `public_keys` table, +/// and returns them in a HashMap. /// -/// Returns the set of mail recipient addresses for which valid gossip headers were found. -async fn update_gossip_peerstates( +/// * `from`: The address which sent the message currently being parsed +async fn parse_gossip_headers( context: &Context, from: &str, recipients: &[SingleInfo], @@ -1926,7 +1925,7 @@ async fn update_gossip_peerstates( continue; } if addr_cmp(from, &header.addr) { - // Non-standard, but anyway we can't update the cached peerstate here. + // Non-standard, might not be necessary to have this check here warn!( context, "Ignoring gossiped \"{}\" as it equals the From address", &header.addr, diff --git a/src/test_utils.rs b/src/test_utils.rs index 98af9c084d..9b43fc7a75 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -626,9 +626,9 @@ impl TestContext { /// Parses a message. /// /// Parsing a message does not run the entire receive pipeline, but is not without - /// side-effects either. E.g. if the message includes autocrypt headers the relevant - /// peerstates will be updated. Later receiving the message using [Self.recv_msg()] is - /// unlikely to be affected as the peerstate would be processed again in exactly the + /// side-effects either. E.g. if the message includes autocrypt headers, + /// gossiped public keys will be saved. Later receiving the message using [Self.recv_msg()] is + /// unlikely to be affected as the message would be processed again in exactly the /// same way. pub(crate) async fn parse_msg(&self, msg: &SentMessage<'_>) -> MimeMessage { MimeMessage::from_bytes(&self.ctx, msg.payload().as_bytes(), None) @@ -1367,7 +1367,6 @@ fn print_logevent(logevent: &LogEvent) { } /// Saves the other account's public key as verified -/// and peerstate as backwards verified. pub(crate) async fn mark_as_verified(this: &TestContext, other: &TestContext) { let contact_id = this.add_or_lookup_contact_id(other).await; mark_contact_id_as_verified(this, contact_id, ContactId::SELF) diff --git a/src/tests/verified_chats.rs b/src/tests/verified_chats.rs index c7c0e3b41a..2ab530ba82 100644 --- a/src/tests/verified_chats.rs +++ b/src/tests/verified_chats.rs @@ -7,6 +7,7 @@ use crate::chat::{ use crate::config::Config; use crate::constants::Chattype; use crate::contact::{Contact, ContactId}; +use crate::key::{load_self_public_key, DcKey}; use crate::message::Message; use crate::mimefactory::MimeFactory; use crate::mimeparser::SystemMessage; @@ -173,17 +174,23 @@ async fn test_create_verified_oneonone_chat() -> Result<()> { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_missing_peerstate_reexecute_securejoin() -> Result<()> { +async fn test_missing_key_reexecute_securejoin() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = &tcm.alice().await; - let alice_addr = alice.get_config(Config::Addr).await?.unwrap(); let bob = &tcm.bob().await; enable_verified_oneonone_chats(&[alice, bob]).await; let chat_id = tcm.execute_securejoin(bob, alice).await; let chat = Chat::load_from_db(bob, chat_id).await?; assert!(chat.is_protected()); bob.sql - .execute("DELETE FROM acpeerstates WHERE addr=?", (&alice_addr,)) + .execute( + "DELETE FROM public_keys WHERE fingerprint=?", + (&load_self_public_key(alice) + .await + .unwrap() + .dc_fingerprint() + .hex(),), + ) .await?; let chat_id = tcm.execute_securejoin(bob, alice).await; let chat = Chat::load_from_db(bob, chat_id).await?; From 25d95209fa9815e3bf39fff2fb7c9cd297dc3641 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Thu, 15 May 2025 18:32:51 +0200 Subject: [PATCH 238/381] Update src/contact.rs --- src/contact.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/contact.rs b/src/contact.rs index 5804a7a828..34961675ae 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -807,7 +807,9 @@ impl Contact { } /// Lookup a contact and create it if it does not exist yet. - /// The contact is identified by the email-address, a name and an "origin" can be given. +/// If `fingerprint` is non-empty, a PGP-contact with this fingerprint is added / looked up. +/// Otherwise, an email-contact with `addr` is added / looked up. +/// A name and an "origin" can be given. /// /// The "origin" is where the address comes from - /// from-header, cc-header, addressbook, qr, manual-edit etc. From 8946ef36a49c6692ac2f5e02ca5e4cf8b4552ec9 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Thu, 15 May 2025 18:55:57 +0200 Subject: [PATCH 239/381] Update src/imap.rs --- src/imap.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/imap.rs b/src/imap.rs index e8c6549fd0..127e057e9d 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -1886,7 +1886,7 @@ async fn should_move_out_of_spam( }; // No chat found. let (from_id, blocked_contact, _origin) = - match from_field_to_contact_id(context, &from, None, true, false) + match from_field_to_contact_id(context, &from, None, true, true) .await .context("from_field_to_contact_id")? { From 2b9b6aae1a846ca406b88ab7c3f0154be13d0b1a Mon Sep 17 00:00:00 2001 From: Hocuri Date: Thu, 15 May 2025 18:56:16 +0200 Subject: [PATCH 240/381] Update src/headerdef.rs --- src/headerdef.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/headerdef.rs b/src/headerdef.rs index 95236c245d..6bae48530c 100644 --- a/src/headerdef.rs +++ b/src/headerdef.rs @@ -75,7 +75,10 @@ pub enum HeaderDef { /// for members listed in the `Chat-Group-Past-Members` field. ChatGroupMemberTimestamps, - /// Fingerprints of the To header. + /// Space-separated PGP key fingerprints + /// of group members listed in the `To` field + /// followed by fingerprints + /// of past members listed in the `Chat-Group-Past-Members` field. ChatGroupMemberFpr, /// Duration of the attached media file. From 088746c39cc7fc1ec7280804125efd1cd40c5bb2 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Thu, 15 May 2025 18:56:54 +0200 Subject: [PATCH 241/381] Update src/contact.rs --- src/contact.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/contact.rs b/src/contact.rs index 34961675ae..43c00a6773 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -425,7 +425,9 @@ pub struct Contact { /// E-Mail-Address of the contact. It is recommended to use `Contact::get_addr` to access this field. addr: String, - /// OpenPGP fingerprint. + /// OpenPGP key fingerprint. + /// Non-empty iff the contact is a pgp-contact, + /// identified by this fingerprint. fingerprint: Option, /// Blocked state. Use contact_is_blocked to access this field. From 514c116436713b24b56ef1b99e9a748768fcc641 Mon Sep 17 00:00:00 2001 From: l Date: Fri, 16 May 2025 16:02:04 +0000 Subject: [PATCH 242/381] Update src/chat.rs Co-authored-by: Hocuri --- src/chat.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat.rs b/src/chat.rs index 0a0b49adf6..0c2475253d 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -4830,7 +4830,7 @@ pub(crate) enum SyncId { // E-mail address of the contact. ContactAddr(String), - // OpenPGP fingerprint of the contact. + /// OpenPGP key fingerprint of the contact. ContactFingerprint(String), Grpid(String), From af11af98646898e06addc921feabaf3191901302 Mon Sep 17 00:00:00 2001 From: l Date: Fri, 16 May 2025 16:02:21 +0000 Subject: [PATCH 243/381] Update src/chat.rs Co-authored-by: Hocuri --- src/chat.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat.rs b/src/chat.rs index 0c2475253d..26af9d5669 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -4827,7 +4827,7 @@ async fn set_contacts_by_fingerprints( /// A cross-device chat id used for synchronisation. #[derive(Debug, Serialize, Deserialize, PartialEq)] pub(crate) enum SyncId { - // E-mail address of the contact. + /// E-mail address of the contact. ContactAddr(String), /// OpenPGP key fingerprint of the contact. From c43645a6c292351e71ce6ea3b703ed1c8b88e346 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 16 May 2025 20:10:17 +0000 Subject: [PATCH 244/381] fmt --- src/contact.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/contact.rs b/src/contact.rs index 43c00a6773..75992ab655 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -809,9 +809,9 @@ impl Contact { } /// Lookup a contact and create it if it does not exist yet. -/// If `fingerprint` is non-empty, a PGP-contact with this fingerprint is added / looked up. -/// Otherwise, an email-contact with `addr` is added / looked up. -/// A name and an "origin" can be given. + /// If `fingerprint` is non-empty, a PGP-contact with this fingerprint is added / looked up. + /// Otherwise, an email-contact with `addr` is added / looked up. + /// A name and an "origin" can be given. /// /// The "origin" is where the address comes from - /// from-header, cc-header, addressbook, qr, manual-edit etc. From 2e9531db024acfee8780f9bc6201db30e23d53af Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 16 May 2025 20:10:17 +0000 Subject: [PATCH 245/381] move parent message lookup --- src/receive_imf.rs | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index bfb218fc3d..e3001d0a41 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -322,6 +322,23 @@ pub(crate) async fn receive_imf_inner( } }; + // Lookup parent message. + // + // This may be useful to assign the message to + // group chats without Chat-Group-ID + // when a message is sent by Thunderbird. + // + // This can be also used to lookup + // PGP-contact by email address + // when receiving a private 1:1 reply + // to a group chat message. + let parent_message = get_parent_message( + context, + mime_parser.get_header(HeaderDef::References), + mime_parser.get_header(HeaderDef::InReplyTo), + ) + .await?; + // ID of the chat to look up the addresses in. // // Note that this is not necessarily the chat we want to assign the message to. @@ -506,6 +523,7 @@ pub(crate) async fn receive_imf_inner( replace_msg_id, prevent_rename, verified_encryption, + parent_message, ) .await .context("add_parts error")? @@ -832,6 +850,7 @@ async fn add_parts( mut replace_msg_id: Option, prevent_rename: bool, verified_encryption: VerifiedEncryption, + parent_message: Option, ) -> Result { let is_bot = context.get_config_bool(Config::Bot).await?; let rfc724_mid_orig = &mime_parser @@ -847,18 +866,12 @@ async fn add_parts( better_msg = Some(stock_str::msg_location_enabled_by(context, from_id).await); } - let parent = get_parent_message( - context, - mime_parser.get_header(HeaderDef::References), - mime_parser.get_header(HeaderDef::InReplyTo), - ) - .await? - .filter(|p| Some(p.id) != replace_msg_id); + let parent_message = parent_message.filter(|p| Some(p.id) != replace_msg_id); let is_dc_message = if mime_parser.has_chat_version() { MessengerMessage::Yes - } else if let Some(parent) = &parent { - match parent.is_dc_message { + } else if let Some(parent_message) = &parent_message { + match parent_message.is_dc_message { MessengerMessage::No => MessengerMessage::No, MessengerMessage::Yes | MessengerMessage::Reply => MessengerMessage::Reply, } @@ -988,7 +1001,7 @@ async fn add_parts( if let Some((new_chat_id, new_chat_id_blocked)) = lookup_chat_or_create_adhoc_group( context, mime_parser, - &parent, + &parent_message, to_ids, from_id, allow_creation || test_normal_chat.is_some(), @@ -1094,7 +1107,7 @@ async fn add_parts( if chat_id_blocked != create_blocked { chat_id.set_blocked(context, create_blocked).await?; } - if create_blocked == Blocked::Request && parent.is_some() { + if create_blocked == Blocked::Request && parent_message.is_some() { // we do not want any chat to be created implicitly. Because of the origin-scale-up, // the contact requests will pop up and this should be just fine. ContactId::scaleup_origin(context, &[from_id], Origin::IncomingReplyTo) @@ -1251,7 +1264,7 @@ async fn add_parts( if let Some((new_chat_id, new_chat_id_blocked)) = lookup_chat_or_create_adhoc_group( context, mime_parser, - &parent, + &parent_message, to_ids, from_id, allow_creation, From c9493e76fd046cc65f8b261b032fbb297abd9b4a Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 16 May 2025 20:54:43 +0000 Subject: [PATCH 246/381] add debug assertion --- src/receive_imf.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index e3001d0a41..fb0f893373 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -3309,6 +3309,7 @@ async fn add_or_lookup_pgp_contacts_by_address_list( } } + debug_assert_eq!(contact_ids.len(), address_list.len()); Ok(contact_ids) } From f7e609dfa00865a26a5515269fccca323837afdf Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 17 May 2025 19:52:09 +0000 Subject: [PATCH 247/381] trash undecryptable messages before chat assignment --- src/receive_imf.rs | 58 +++++++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index fb0f893373..5a8d4bad75 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1200,6 +1200,33 @@ async fn add_parts( chat_id = Some(DC_CHAT_ID_TRASH); } + if mime_parser.decrypting_failed { + chat_id = Some(DC_CHAT_ID_TRASH); + let last_time = context + .get_config_i64(Config::LastCantDecryptOutgoingMsgs) + .await?; + let now = tools::time(); + let update_config = if last_time.saturating_add(24 * 60 * 60) <= now { + let mut msg = + Message::new_text(stock_str::cant_decrypt_outgoing_msgs(context).await); + chat::add_device_msg(context, None, Some(&mut msg)) + .await + .log_err(context) + .ok(); + true + } else { + last_time > now + }; + if update_config { + context + .set_config_internal( + Config::LastCantDecryptOutgoingMsgs, + Some(&now.to_string()), + ) + .await?; + } + } + // Try to assign to a chat based on Chat-Group-ID. if chat_id.is_none() { if let Some(grpid) = mime_parser.get_chat_group_id().map(|s| s.to_string()) { @@ -1229,37 +1256,6 @@ async fn add_parts( } } - if mime_parser.decrypting_failed { - if chat_id.is_none() { - chat_id = Some(DC_CHAT_ID_TRASH); - } else { - hidden = true; - } - let last_time = context - .get_config_i64(Config::LastCantDecryptOutgoingMsgs) - .await?; - let now = tools::time(); - let update_config = if last_time.saturating_add(24 * 60 * 60) <= now { - let mut msg = - Message::new_text(stock_str::cant_decrypt_outgoing_msgs(context).await); - chat::add_device_msg(context, None, Some(&mut msg)) - .await - .log_err(context) - .ok(); - true - } else { - last_time > now - }; - if update_config { - context - .set_config_internal( - Config::LastCantDecryptOutgoingMsgs, - Some(&now.to_string()), - ) - .await?; - } - } - if chat_id.is_none() { if let Some((new_chat_id, new_chat_id_blocked)) = lookup_chat_or_create_adhoc_group( context, From cf0ae7ba113dc9396b24fcac796078124abd4044 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 17 May 2025 20:49:55 +0000 Subject: [PATCH 248/381] clippy --- src/receive_imf.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 5a8d4bad75..59ba4da2e6 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -912,7 +912,7 @@ async fn add_parts( // (of course, the user can add other chats manually later) let to_id: ContactId; let state: MessageState; - let mut hidden = is_reaction; + let hidden = is_reaction; let mut needs_delete_job = false; let mut restore_protection = false; From af4e3fe1bc13dd563da4b35291c4ce22613b33d5 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 17 May 2025 21:53:01 +0000 Subject: [PATCH 249/381] test fixup after merge --- deltachat-rpc-client/tests/test_securejoin.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/deltachat-rpc-client/tests/test_securejoin.py b/deltachat-rpc-client/tests/test_securejoin.py index 754a715cac..31f4835207 100644 --- a/deltachat-rpc-client/tests/test_securejoin.py +++ b/deltachat-rpc-client/tests/test_securejoin.py @@ -248,22 +248,11 @@ def test_verified_group_member_added_recovery(acfactory) -> None: assert len(ac3_chat.get_contacts()) == 3 ac3_chat.send_text("Hi!") - msg_id = ac2.wait_for_incoming_msg_event().msg_id - message = ac2.get_message_by_id(msg_id) - snapshot = message.get_snapshot() - logging.info("Received message %s", snapshot.text) - assert snapshot.text == "[...] – [This message was encrypted for another setup.]" - ac1.wait_for_incoming_msg_event() # Hi! ac3_contact_ac2 = ac3.create_contact(ac2) ac3_chat.remove_contact(ac3_contact_ac2_old) - msg_id = ac2.wait_for_incoming_msg_event().msg_id - message = ac2.get_message_by_id(msg_id) - snapshot = message.get_snapshot() - assert snapshot.text == "[...] – [This message was encrypted for another setup.]" - snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot() assert "removed" in snapshot.text From 2db72ae5639db7a6e3e915678324b4f4d4f0dfdf Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 17 May 2025 22:31:57 +0000 Subject: [PATCH 250/381] rename get_pgp_chat into get_chat --- src/chat/chat_tests.rs | 18 +++++++++--------- src/receive_imf/receive_imf_tests.rs | 4 ++-- src/securejoin/securejoin_tests.rs | 12 ++++++------ src/test_utils.rs | 6 +++--- src/tests/verified_chats.rs | 15 ++++++--------- 5 files changed, 26 insertions(+), 29 deletions(-) diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index f8651c6aaf..20482a9654 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -2325,7 +2325,7 @@ async fn test_saved_msgs_not_added_to_shared_chats() -> Result<()> { assert_eq!(shared_chats.len(), 1); assert_eq!( shared_chats.get_chat_id(0).unwrap(), - bob.get_pgp_chat(&alice).await.id + bob.get_chat(&alice).await.id ); Ok(()) @@ -2943,16 +2943,16 @@ async fn test_sync_blocked() -> Result<()> { alice1.recv_msg(&sent_msg).await; let a0b_contact_id = alice0.add_or_lookup_contact_id(bob).await; - assert_eq!(alice1.get_pgp_chat(bob).await.blocked, Blocked::Request); + assert_eq!(alice1.get_chat(bob).await.blocked, Blocked::Request); a0b_chat_id.accept(alice0).await?; sync(alice0, alice1).await; - assert_eq!(alice1.get_pgp_chat(bob).await.blocked, Blocked::Not); + assert_eq!(alice1.get_chat(bob).await.blocked, Blocked::Not); a0b_chat_id.block(alice0).await?; sync(alice0, alice1).await; - assert_eq!(alice1.get_pgp_chat(bob).await.blocked, Blocked::Yes); + assert_eq!(alice1.get_chat(bob).await.blocked, Blocked::Yes); a0b_chat_id.unblock(alice0).await?; sync(alice0, alice1).await; - assert_eq!(alice1.get_pgp_chat(bob).await.blocked, Blocked::Not); + assert_eq!(alice1.get_chat(bob).await.blocked, Blocked::Not); // Unblocking a 1:1 chat doesn't unblock the contact currently. Contact::unblock(alice0, a0b_contact_id).await?; @@ -3016,7 +3016,7 @@ async fn test_sync_accept_before_first_msg() -> Result<()> { a0b_chat_id.accept(alice0).await?; let a0b_contact = Contact::get_by_id(alice0, a0b_contact_id).await?; assert_eq!(a0b_contact.origin, Origin::CreateChat); - assert_eq!(alice0.get_pgp_chat(bob).await.blocked, Blocked::Not); + assert_eq!(alice0.get_chat(bob).await.blocked, Blocked::Not); sync(alice0, alice1).await; let alice1_contacts = Contact::get_all(alice1, 0, None).await?; @@ -3025,7 +3025,7 @@ async fn test_sync_accept_before_first_msg() -> Result<()> { let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?; assert_eq!(a1b_contact.get_addr(), ""); assert_eq!(a1b_contact.origin, Origin::CreateChat); - let a1b_chat = alice1.get_pgp_chat(bob).await; + let a1b_chat = alice1.get_chat(bob).await; assert_eq!(a1b_chat.blocked, Blocked::Not); let chats = Chatlist::try_load(alice1, 0, None, None).await?; assert_eq!(chats.len(), 1); @@ -3238,7 +3238,7 @@ async fn test_sync_muted() -> Result<()> { alice1.create_chat(&bob).await; assert_eq!( - alice1.get_pgp_chat(&bob).await.mute_duration, + alice1.get_chat(&bob).await.mute_duration, MuteDuration::NotMuted ); let mute_durations = [ @@ -3256,7 +3256,7 @@ async fn test_sync_muted() -> Result<()> { ), _ => m, }; - assert_eq!(alice1.get_pgp_chat(&bob).await.mute_duration, m); + assert_eq!(alice1.get_chat(&bob).await.mute_duration, m); } Ok(()) } diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index cd46832462..5253aab873 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -2978,7 +2978,7 @@ async fn test_outgoing_private_reply_multidevice() -> Result<()> { let received = alice2.get_last_msg().await; // That's a regression test for https://github.com/chatmail/core/issues/2949: - assert_eq!(received.chat_id, alice2.get_pgp_chat(&bob).await.id); + assert_eq!(received.chat_id, alice2.get_chat(&bob).await.id); let alice2_bob_contact = alice2.add_or_lookup_contact(&bob).await; assert_eq!(received.from_id, ContactId::SELF); @@ -4767,7 +4767,7 @@ async fn test_prefer_references_to_downloaded_msgs() -> Result<()> { let received = bob.recv_msg(&sent).await; assert_eq!(received.download_state, DownloadState::Available); assert_ne!(received.chat_id, bob_chat_id); - assert_eq!(received.chat_id, bob.get_pgp_chat(alice).await.id); + assert_eq!(received.chat_id, bob.get_chat(alice).await.id); let mut msg = Message::new(Viewtype::File); msg.set_file_from_bytes(alice, "file", file_bytes, None)?; diff --git a/src/securejoin/securejoin_tests.rs b/src/securejoin/securejoin_tests.rs index 086873b309..d565fe9f0a 100644 --- a/src/securejoin/securejoin_tests.rs +++ b/src/securejoin/securejoin_tests.rs @@ -137,12 +137,12 @@ async fn test_setup_contact_ex(case: SetupContactCase) { "vc-auth-required" ); - let bob_chat = bob.get_pgp_chat(&alice).await; + let bob_chat = bob.get_chat(&alice).await; assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true); tcm.section("Step 4: Bob receives vc-auth-required, sends vc-request-with-auth"); bob.recv_msg_trash(&sent).await; - let bob_chat = bob.get_pgp_chat(&alice).await; + let bob_chat = bob.get_chat(&alice).await; assert_eq!(bob_chat.why_cant_send(&bob).await.unwrap(), None); assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true); @@ -230,7 +230,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) { assert_eq!(contact_bob.is_bot(), false); // exactly one one-to-one chat should be visible for both now - // (check this before calling alice.get_pgp_chat() explicitly below) + // (check this before calling alice.get_chat() explicitly below) assert_eq!( Chatlist::try_load(&alice, 0, None, None) .await @@ -245,7 +245,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) { // Check Alice got the verified message in her 1:1 chat. { - let chat = alice.get_pgp_chat(&bob).await; + let chat = alice.get_chat(&bob).await; let msg = get_chat_msg(&alice, chat.get_id(), 0, 1).await; assert!(msg.is_info()); let expected_text = chat_protection_enabled(&alice).await; @@ -530,7 +530,7 @@ async fn test_secure_join() -> Result<()> { // Now Alice's chat with Bob should still be hidden, the verified message should // appear in the group chat. - let chat = alice.get_pgp_chat(&bob).await; + let chat = alice.get_chat(&bob).await; assert_eq!( chat.blocked, Blocked::Yes, @@ -556,7 +556,7 @@ async fn test_secure_join() -> Result<()> { { // Bob has Alice verified, message shows up in the group chat. assert_eq!(contact_alice.is_verified(&bob).await?, true); - let chat = bob.get_pgp_chat(&alice).await; + let chat = bob.get_chat(&alice).await; assert_eq!( chat.blocked, Blocked::Yes, diff --git a/src/test_utils.rs b/src/test_utils.rs index 9b43fc7a75..23e1cab992 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -800,7 +800,7 @@ impl TestContext { .map(|chat_id_blocked| chat_id_blocked.id) .expect( "There is no chat with this contact. \ - Hint: Use create_chat() instead of get_chat() if this is expected.", + Hint: Use create_email_chat() instead of get_email_chat() if this is expected.", ); Chat::load_from_db(&self.ctx, chat_id).await.unwrap() @@ -812,7 +812,7 @@ impl TestContext { /// This first creates a contact, but does not import the key, /// so may create a PGP-contact with a fingerprint /// but without the key. - pub async fn get_pgp_chat(&self, other: &TestContext) -> Chat { + pub async fn get_chat(&self, other: &TestContext) -> Chat { let contact = self.add_or_lookup_contact_id(other).await; let chat_id = ChatIdBlocked::lookup_by_contact(&self.ctx, contact) @@ -821,7 +821,7 @@ impl TestContext { .map(|chat_id_blocked| chat_id_blocked.id) .expect( "There is no chat with this contact. \ - Hint: Use create_chat() instead of get_pgp_chat() if this is expected.", + Hint: Use create_chat() instead of get_chat() if this is expected.", ); Chat::load_from_db(&self.ctx, chat_id).await.unwrap() diff --git a/src/tests/verified_chats.rs b/src/tests/verified_chats.rs index 46569dcdd1..27d84e70a8 100644 --- a/src/tests/verified_chats.rs +++ b/src/tests/verified_chats.rs @@ -126,7 +126,7 @@ async fn test_create_verified_oneonone_chat() -> Result<()> { // Alice should have a hidden protected chat with Fiona { - let chat = alice.get_pgp_chat(&fiona).await; + let chat = alice.get_chat(&fiona).await; assert!(chat.is_protected()); let msg = get_chat_msg(&alice, chat.id, 0, 1).await; @@ -136,7 +136,7 @@ async fn test_create_verified_oneonone_chat() -> Result<()> { // Fiona should have a hidden protected chat with Alice { - let chat = fiona.get_pgp_chat(&alice).await; + let chat = fiona.get_chat(&alice).await; assert!(chat.is_protected()); let msg0 = get_chat_msg(&fiona, chat.id, 0, 1).await; @@ -157,7 +157,7 @@ async fn test_create_verified_oneonone_chat() -> Result<()> { // Alice gets a new unprotected chat with new Fiona contact. { - let chat = alice.get_pgp_chat(&fiona_new).await; + let chat = alice.get_chat(&fiona_new).await; assert!(!chat.is_protected()); let msg = get_chat_msg(&alice, chat.id, 0, 1).await; @@ -413,10 +413,7 @@ async fn test_outgoing_mua_msg() -> Result<()> { .golden_test_chat(sent.chat_id, "test_outgoing_mua_msg") .await; alice - .golden_test_chat( - alice.get_pgp_chat(&bob).await.id, - "test_outgoing_mua_msg_pgp", - ) + .golden_test_chat(alice.get_chat(&bob).await.id, "test_outgoing_mua_msg_pgp") .await; Ok(()) @@ -509,7 +506,7 @@ async fn test_message_from_old_dc_setup() -> Result<()> { // The outdated Bob's Autocrypt header isn't applied // and the message goes to another chat, so the verification preserves. assert!(contact.is_verified(alice).await.unwrap()); - let chat = alice.get_pgp_chat(bob).await; + let chat = alice.get_chat(bob).await; assert!(chat.is_protected()); assert_eq!(chat.is_protection_broken(), false); Ok(()) @@ -726,7 +723,7 @@ async fn assert_verified(this: &TestContext, other: &TestContext, protected: Pro assert_eq!(contact.is_verified(this).await.unwrap(), true); } - let chat = this.get_pgp_chat(other).await; + let chat = this.get_chat(other).await; let (expect_protected, expect_broken) = match protected { ProtectionStatus::Unprotected => (false, false), ProtectionStatus::Protected => (true, false), From b7a153872ff99ec5c9c02c3f5788af51bcc926d8 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 18 May 2025 01:57:15 +0000 Subject: [PATCH 251/381] replace ref with & --- src/mimefactory.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 862fe3e594..46bc6948a7 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -284,7 +284,7 @@ impl MimeFactory { for row in rows { let (authname, addr, fingerprint, id, add_timestamp, remove_timestamp, public_key_bytes_opt) = row?; - let public_key_opt = if let Some(ref public_key_bytes) = public_key_bytes_opt { + let public_key_opt = if let Some(public_key_bytes) = &public_key_bytes_opt { Some(SignedPublicKey::from_slice(public_key_bytes)?) } else { None From b0d52c2adddff11f32caaebf33fa8a1a3018af58 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 18 May 2025 02:24:00 +0000 Subject: [PATCH 252/381] replace parse with from_str --- src/mimeparser.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 248ba969b3..f7afd749bd 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -1870,7 +1870,7 @@ impl MimeMessage { if let Some(header) = self.get_header(HeaderDef::ChatGroupMemberFpr) { header .split_ascii_whitespace() - .filter_map(|fpr| fpr.parse::().ok()) + .filter_map(|fpr| Fingerprint::from_str(fpr).ok()) .collect() } else { Vec::new() From 7f29744e974b2c7842792ca1aa6221bf727a829e Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 18 May 2025 07:04:45 +0000 Subject: [PATCH 253/381] remove outdated comment --- src/receive_imf.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index d995e37288..74c29fdec4 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -3140,10 +3140,6 @@ async fn has_verified_encryption( return Ok(Verified); } - // ensure, the contact is verified - // and the message is signed with a verified key of the sender. - // this check is skipped for SELF as there is no proper SELF-peerstate - // and results in group-splits otherwise. let from_contact = Contact::get_by_id(context, from_id).await?; let Some(fingerprint) = from_contact.fingerprint() else { From 1e89784fc8b28c19f0c48b85d0c0769e03caad6f Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 18 May 2025 07:16:25 +0000 Subject: [PATCH 254/381] check that fingerprint is non-empty --- src/receive_imf.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 74c29fdec4..ac8e0380b9 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -3307,6 +3307,10 @@ async fn add_or_lookup_pgp_contacts_by_address_list( Ok(contact_ids) } +/// Looks up a PGP-contact by email address. +/// +/// Provided `chat_id` must be an encrypted +/// chat ID that has PGP-contacts inside. async fn lookup_pgp_contact_by_address( context: &Context, addr: &str, @@ -3331,7 +3335,9 @@ async fn lookup_pgp_contact_by_address( WHERE contacts.addr=? AND EXISTS (SELECT 1 FROM chats_contacts WHERE contact_id=contacts.id - AND chat_id=?)", + AND chat_id=?) + AND fingerprint<>'' -- Should always be true + ", (addr, chat_id), |row| { let contact_id: ContactId = row.get(0)?; From 207d601d4937cdc1a7dfcfd3c4c2f7e823b296d1 Mon Sep 17 00:00:00 2001 From: l Date: Tue, 20 May 2025 15:08:48 +0000 Subject: [PATCH 255/381] Update src/tests/aeap.rs Co-authored-by: Hocuri --- src/tests/aeap.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/tests/aeap.rs b/src/tests/aeap.rs index 6883d4c945..526dce65b1 100644 --- a/src/tests/aeap.rs +++ b/src/tests/aeap.rs @@ -1,3 +1,12 @@ +//! "AEAP" means "Automatic Email Address Porting" +//! and was the predecessor of PGP-contacts +//! (i.e. identifying contacts via the PGP fingerprint, +//! while allowing the email address to change). +//! +//! These tests still pass because PGP-contacts +//! allows messaging to continue after an email address change, +//! just as AEAP did. Some other tests had to be removed. + use anyhow::Result; use crate::chat::{self, Chat, ChatId, ProtectionStatus}; From ba65f290fe854e9ccb02c0635ecadd1ddd10c71f Mon Sep 17 00:00:00 2001 From: iequidoo Date: Mon, 19 May 2025 10:21:30 -0300 Subject: [PATCH 256/381] fix: Assign encrypted message to PGP chat if any PGP contacts found (#6856) Particularly, this fixes a scenario when the second device sends a message to the 1:1 chat right after SecureJoin. --- src/receive_imf.rs | 122 ++++++++++++------- src/tests/verified_chats.rs | 17 +++ test-data/golden/test_outgoing_encrypted_msg | 5 + 3 files changed, 101 insertions(+), 43 deletions(-) create mode 100644 test-data/golden/test_outgoing_encrypted_msg diff --git a/src/receive_imf.rs b/src/receive_imf.rs index ac8e0380b9..af1de3cca2 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -412,7 +412,7 @@ pub(crate) async fn receive_imf_inner( context, &mime_parser.past_members, past_member_fingerprints, - chat_id, + Some(chat_id), ) .await?; } else { @@ -435,27 +435,42 @@ pub(crate) async fn receive_imf_inner( // mapped it to a PGP contact. // This is a 1:1 PGP-chat. to_ids = pgp_to_ids - } else if let Some(chat_id) = chat_id { - to_ids = lookup_pgp_contacts_by_address_list( - context, - &mime_parser.recipients, - to_member_fingerprints, - chat_id, - ) - .await?; } else { - to_ids = add_or_lookup_contacts_by_address_list( - context, - &mime_parser.recipients, - if !mime_parser.incoming { - Origin::OutgoingTo - } else if incoming_origin.is_known() { - Origin::IncomingTo - } else { - Origin::IncomingUnknownTo - }, - ) - .await?; + let ids = match mime_parser.was_encrypted() { + true => { + lookup_pgp_contacts_by_address_list( + context, + &mime_parser.recipients, + to_member_fingerprints, + chat_id, + ) + .await? + } + false => vec![], + }; + if chat_id.is_some() + || (mime_parser.was_encrypted() && !ids.contains(&None)) + // Prefer creating PGP chats if there are any PGP contacts. At least this prevents + // from replying unencrypted. + || ids + .iter() + .any(|&c| c.is_some() && c != Some(ContactId::SELF)) + { + to_ids = ids; + } else { + to_ids = add_or_lookup_contacts_by_address_list( + context, + &mime_parser.recipients, + if !mime_parser.incoming { + Origin::OutgoingTo + } else if incoming_origin.is_known() { + Origin::IncomingTo + } else { + Origin::IncomingUnknownTo + }, + ) + .await?; + } } past_ids = add_or_lookup_contacts_by_address_list( @@ -2523,7 +2538,7 @@ async fn apply_group_changes( } if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) { - removed_id = lookup_pgp_contact_by_address(context, removed_addr, chat_id).await?; + removed_id = lookup_pgp_contact_by_address(context, removed_addr, Some(chat_id)).await?; if let Some(id) = removed_id { better_msg = if id == from_id { silent = true; @@ -3309,12 +3324,12 @@ async fn add_or_lookup_pgp_contacts_by_address_list( /// Looks up a PGP-contact by email address. /// -/// Provided `chat_id` must be an encrypted -/// chat ID that has PGP-contacts inside. +/// If provided, `chat_id` must be an encrypted chat ID that has PGP-contacts inside. +/// Otherwise the function searches in all contacts, returning the recently seen one. async fn lookup_pgp_contact_by_address( context: &Context, addr: &str, - chat_id: ChatId, + chat_id: Option, ) -> Result> { if context.is_self_addr(addr).await? { let is_self_in_chat = context @@ -3328,23 +3343,44 @@ async fn lookup_pgp_contact_by_address( return Ok(Some(ContactId::SELF)); } } - let contact_id: Option = context - .sql - .query_row_optional( - "SELECT id FROM contacts - WHERE contacts.addr=? - AND EXISTS (SELECT 1 FROM chats_contacts - WHERE contact_id=contacts.id - AND chat_id=?) - AND fingerprint<>'' -- Should always be true - ", - (addr, chat_id), - |row| { - let contact_id: ContactId = row.get(0)?; - Ok(contact_id) - }, - ) - .await?; + let contact_id: Option = match chat_id { + Some(chat_id) => { + context + .sql + .query_row_optional( + "SELECT id FROM contacts + WHERE contacts.addr=? + AND EXISTS (SELECT 1 FROM chats_contacts + WHERE contact_id=contacts.id + AND chat_id=?) + AND fingerprint<>'' -- Should always be true + ", + (addr, chat_id), + |row| { + let contact_id: ContactId = row.get(0)?; + Ok(contact_id) + }, + ) + .await? + } + None => { + context + .sql + .query_row_optional( + "SELECT id FROM contacts + WHERE contacts.addr=?1 + AND fingerprint<>'' + ORDER BY last_seen DESC, id DESC + ", + (addr,), + |row| { + let contact_id: ContactId = row.get(0)?; + Ok(contact_id) + }, + ) + .await? + } + }; Ok(contact_id) } @@ -3396,7 +3432,7 @@ async fn lookup_pgp_contacts_by_address_list( context: &Context, address_list: &[SingleInfo], fingerprints: &[Fingerprint], - chat_id: ChatId, + chat_id: Option, ) -> Result>> { let mut contact_ids = Vec::new(); let mut fingerprint_iter = fingerprints.iter(); diff --git a/src/tests/verified_chats.rs b/src/tests/verified_chats.rs index 27d84e70a8..04bd2d9c6b 100644 --- a/src/tests/verified_chats.rs +++ b/src/tests/verified_chats.rs @@ -419,6 +419,23 @@ async fn test_outgoing_mua_msg() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_outgoing_encrypted_msg() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + enable_verified_oneonone_chats(&[alice]).await; + + mark_as_verified(alice, bob).await; + let chat_id = alice.create_chat(bob).await.id; + let raw = include_bytes!("../../test-data/message/thunderbird_with_autocrypt.eml"); + receive_imf(alice, raw, false).await?; + alice + .golden_test_chat(chat_id, "test_outgoing_encrypted_msg") + .await; + Ok(()) +} + /// If Bob answers unencrypted from another address with a classical MUA, /// the message is under some circumstances still assigned to the original /// chat (see lookup_chat_by_reply()); this is meant to make aliases diff --git a/test-data/golden/test_outgoing_encrypted_msg b/test-data/golden/test_outgoing_encrypted_msg new file mode 100644 index 0000000000..d0c6e09a6e --- /dev/null +++ b/test-data/golden/test_outgoing_encrypted_msg @@ -0,0 +1,5 @@ +Single#Chat#10: bob@example.net [PGP bob@example.net] 🛡️ +-------------------------------------------------------------------------------- +Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️] +Msg#11🔒: Me (Contact#Contact#Self): Test – This is encrypted, signed, and has an Autocrypt Header without prefer-encrypt=mutual. √ +-------------------------------------------------------------------------------- From 72614b062ad8491db539ac7bbe3f8363d5da3981 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 15:08:28 +0000 Subject: [PATCH 257/381] test: add failing test where email contacts are added to PGP-chat --- src/receive_imf.rs | 5 +++ src/receive_imf/receive_imf_tests.rs | 61 ++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index af1de3cca2..5d1db25ed8 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -2089,6 +2089,11 @@ async fn lookup_chat_by_reply( } } + // Do not assign unencrypted messages to encrypted chats. + if parent_chat.is_encrypted(context).await? && !mime_parser.was_encrypted() { + return Ok(None); + } + info!( context, "Assigning message to {} as it's a reply to {}.", parent_chat.id, parent.rfc724_mid diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 64ef3fdd6d..aff3f43448 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -5087,3 +5087,64 @@ PGh0bWw+PGJvZHk+dGV4dDwvYm9keT5kYXRh Ok(()) } + +/// Tests that email contacts are not added into a group +/// with PGP-contacts by a plaintext reply. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_no_email_contact_added_into_group() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + let alice_chat_id = alice + .create_group_with_members(ProtectionStatus::Unprotected, "Group", &[bob]) + .await; + let bob_received_msg = bob + .recv_msg(&alice.send_text(alice_chat_id, "Message").await) + .await; + let rfc724_mid = bob_received_msg.rfc724_mid; + + // Alice leaves the group so message from email address contact bob@example.com + // does not fail the test for being non-member and is allowed to + // modify the chat. + remove_contact_from_chat(alice, alice_chat_id, ContactId::SELF).await?; + + // Wait 60 days so chatlist is stale. + SystemTime::shift(Duration::from_secs(60 * 24 * 60 * 60 + 1)); + + // Only Bob is the chat member. + assert_eq!( + chat::get_chat_contacts(alice, alice_chat_id).await?.len(), + 1 + ); + + let msg = receive_imf( + alice, + format!( + "From: bob@example.com\n\ + To: alice@example.net, charlie@example.net, fiona@example.net\n\ + Subject: foo\n\ + Message-ID: \n\ + Chat-Version: 1.0\n\ + In-Reply-To: {rfc724_mid}\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + Hello\n" + ) + .as_bytes(), + false, + ) + .await? + .unwrap(); + + // Unencrypted message should not modify the chat member list. + assert_eq!( + chat::get_chat_contacts(alice, alice_chat_id).await?.len(), + 1 + ); + + // Unencrypted message should not even be assigned to encrypted chat. + assert_eq!(msg.chat_id, alice_chat_id); + + Ok(()) +} From 224f4d02eb3818ca6e56ed68edee863b99f02793 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Wed, 28 May 2025 12:54:06 +0200 Subject: [PATCH 258/381] fix: Encrypt broadcast list in PGP-contacts migration (#6858) --- src/sql/migrations.rs | 53 +++++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 82c1750cc6..7231ecd7ae 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1598,8 +1598,30 @@ fn migrate_pgp_contacts( orphaned_contacts.remove(m); } }; + let retain_autocrypt_pgp_contacts = || { + old_members + .iter() + .map(|original| { + ( + *original, + autocrypt_pgp_contacts + .get(original) + // TODO it's unclear whether we want to do this: + // We could also make the group unencrypted + // if any peerstate is reset. + // Also, right now, if we have no key at all, + // the member will be silently removed from the group; + // maybe we should at least post an info message? + .or_else(|| { + autocrypt_pgp_contacts_with_reset_peerstate.get(original) + }) + .copied(), + ) + }) + .collect::)>>() + }; - let old_and_new_members = match typ { + let old_and_new_members: Vec<(u32, Option)> = match typ { // 1:1 chats retain: // - email-contact if peerstate is in the "reset" state, // or if there is no PGP-contact that has the right email address. @@ -1646,36 +1668,19 @@ fn migrate_pgp_contacts( }) .collect() } else { - old_members - .iter() - .map(|original| { - ( - *original, - autocrypt_pgp_contacts - .get(original) - // TODO it's unclear whether we want to do this: - // We could also make the group unencrypted - // if any peerstate is reset. - // Also, right now, if we have no key at all, - // the member will be silently removed from the group; - // maybe we should at least post an info message? - .or_else(|| { - autocrypt_pgp_contacts_with_reset_peerstate - .get(original) - }) - .copied(), - ) - }) - .collect() + retain_autocrypt_pgp_contacts() } } - // Mailinglist | Broadcast list - 140 | 160 => { + // Mailinglist + 140 => { keep_email_contacts("Mailinglist/Broadcast"); continue; } + // Broadcast list + 160 => retain_autocrypt_pgp_contacts(), + _ => { warn!(context, "Invalid chat type {typ}"); continue; From 4dc067477ebb831020684365d72743a85809d397 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 28 May 2025 17:46:30 +0000 Subject: [PATCH 259/381] fix mailinglist reason, no broadcast there --- src/sql/migrations.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 7231ecd7ae..f23e2340f3 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1674,7 +1674,7 @@ fn migrate_pgp_contacts( // Mailinglist 140 => { - keep_email_contacts("Mailinglist/Broadcast"); + keep_email_contacts("Mailinglist"); continue; } From ee53a54c71cebcd27893fd238b563091edb20ce3 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 16 May 2025 20:10:17 +0000 Subject: [PATCH 260/381] Add ChatAssignment enum --- src/receive_imf.rs | 285 ++++++++++++++++++++++++++++++++------------- 1 file changed, 206 insertions(+), 79 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 5d1db25ed8..2fc582a3b5 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -69,6 +69,62 @@ pub struct ReceivedMsg { pub needs_delete_job: bool, } +/// Decision on which kind of chat the message +/// should be assigned in. +/// +/// This is done before looking up contact IDs +/// so we know in advance whether to lookup +/// PGP-contacts or email address contacts. +/// +/// Once this decision is made, +/// it should not be changed so we +/// don't assign the message to an encrypted +/// group after looking up PGP-contacts +/// or vice versa. +enum ChatAssignment { + // Group chat with a Group ID. + // + // Lookup PGP contacts and + // assign to encrypted group + // even if the message itself + // is not encrypted. + GroupChat { + grpid: String, + }, + + /// Mailing list or broadcast list. + /// + /// Mailing lists don't have members. + /// Broadcast lists have members + /// on the sender side, + /// but their addresses don't go into + /// the `To` field. + /// + /// In any case, the `To` + /// field should be ignored + /// and no contact IDs should be looked + /// up except the `from_id` + /// which may be an email address contact + /// or a PGP-contact. + MailingList { + listid: String, + }, + + /// Group chat without a Group ID. + /// + /// This is not encrypted. + AdHocGroup, + + /// 1:1 chat with a single contact. + /// + /// The chat may be encrypted or not, + /// it does not matter. + /// It is not possible to mix + /// email address contacts + /// with PGP-contacts in a single 1:1 chat anyway. + OneOneChat, +} + /// Emulates reception of a message from the network. /// /// This method returns errors on a failure to parse the mail or extract Message-ID. It's only used @@ -339,39 +395,71 @@ pub(crate) async fn receive_imf_inner( ) .await?; + // Decide on the type of chat we assign the message to. + // + // The chat may not exist yet, i.e. there may be + // no database row and ChatId yet. + let chat_assignment = if let Some(grpid) = mime_parser.get_chat_group_id() { + if mime_parser.was_encrypted() { + ChatAssignment::GroupChat { + grpid: grpid.to_string(), + } + } else { + // Could be a message from old version + // with opportunistic encryption. + // + // We still want to assign this to a group + // even if it had only two members. + // + // Group ID is ignored, however. + ChatAssignment::AdHocGroup + } + } else if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() { + let listid = mailinglist_header_listid(mailinglist_header)?; + ChatAssignment::MailingList { listid } + } else if mime_parser.recipients.len() == 1 { + ChatAssignment::OneOneChat + } else { + ChatAssignment::AdHocGroup + }; + // ID of the chat to look up the addresses in. // // Note that this is not necessarily the chat we want to assign the message to. // In case of an outgoing private reply to a group message we may // lookup the address of receipient in the list of addresses used in the group, // but want to assign the message to 1:1 chat. - let chat_id = if let Some(grpid) = mime_parser.get_chat_group_id() { - if let Some((chat_id, _protected, _blocked)) = - chat::get_chat_id_by_grpid(context, grpid).await? - { - Some(chat_id) - } else { - None + let chat_id = match chat_assignment { + ChatAssignment::GroupChat { ref grpid } => { + if let Some((chat_id, _protected, _blocked)) = + chat::get_chat_id_by_grpid(context, grpid).await? + { + Some(chat_id) + } else { + None + } } - } else if is_partial_download.is_none() && !mime_parser.incoming { - if let Some(parent) = get_parent_message( - context, - mime_parser.get_header(HeaderDef::References), - mime_parser.get_header(HeaderDef::InReplyTo), - ) - .await? - { - Some(parent.chat_id) - } else { + ChatAssignment::AdHocGroup => { + // If we are going to assign a message to ad hoc group, + // we can just convert the email addresses + // to e-mail address contacts and don't need a `ChatId` + // to lookup PGP-contacts. None } - } else { - None + ChatAssignment::MailingList { .. } => None, + ChatAssignment::OneOneChat => { + if is_partial_download.is_none() && !mime_parser.incoming { + if let Some(parent_message) = &parent_message { + Some(parent_message.chat_id) + } else { + None + } + } else { + None + } + } }; - let to_ids: Vec>; - let past_ids: Vec>; - let member_fingerprints = mime_parser.chat_group_member_fingerprints(); let to_member_fingerprints; let past_member_fingerprints; @@ -404,81 +492,120 @@ pub(crate) async fn receive_imf_inner( ) .await?; - if mime_parser.get_chat_group_id().is_some() { - to_ids = pgp_to_ids; + let to_ids: Vec>; + let past_ids: Vec>; - if let Some(chat_id) = chat_id { - past_ids = lookup_pgp_contacts_by_address_list( + match chat_assignment { + ChatAssignment::GroupChat { .. } => { + to_ids = pgp_to_ids; + + if let Some(chat_id) = chat_id { + past_ids = lookup_pgp_contacts_by_address_list( + context, + &mime_parser.past_members, + past_member_fingerprints, + Some(chat_id), + ) + .await?; + } else { + past_ids = add_or_lookup_pgp_contacts_by_address_list( + context, + &mime_parser.past_members, + &mime_parser.gossiped_keys, + past_member_fingerprints, + Origin::Hidden, + ) + .await?; + } + } + ChatAssignment::MailingList { .. } => { + to_ids = Vec::new(); + past_ids = Vec::new(); + } + ChatAssignment::AdHocGroup => { + to_ids = add_or_lookup_contacts_by_address_list( context, - &mime_parser.past_members, - past_member_fingerprints, - Some(chat_id), + &mime_parser.recipients, + if !mime_parser.incoming { + Origin::OutgoingTo + } else if incoming_origin.is_known() { + Origin::IncomingTo + } else { + Origin::IncomingUnknownTo + }, ) .await?; - } else { - past_ids = add_or_lookup_pgp_contacts_by_address_list( + + past_ids = add_or_lookup_contacts_by_address_list( context, &mime_parser.past_members, - &mime_parser.gossiped_keys, - past_member_fingerprints, Origin::Hidden, ) .await?; } - } else { - if pgp_to_ids.len() == 1 - && pgp_to_ids - .first() - .is_some_and(|contact_id| contact_id.is_some()) - { - // There is a single recipient and we have - // mapped it to a PGP contact. - // This is a 1:1 PGP-chat. - to_ids = pgp_to_ids - } else { - let ids = match mime_parser.was_encrypted() { - true => { - lookup_pgp_contacts_by_address_list( - context, - &mime_parser.recipients, - to_member_fingerprints, - chat_id, - ) - .await? - } - false => vec![], - }; - if chat_id.is_some() + ChatAssignment::OneOneChat => { + if pgp_to_ids.len() == 1 + && pgp_to_ids + .first() + .is_some_and(|contact_id| contact_id.is_some()) + { + // There is a single recipient and we have + // mapped it to a PGP contact. + // This is a 1:1 PGP-chat. + to_ids = pgp_to_ids + } else if let Some(chat_id) = chat_id { + to_ids = lookup_pgp_contacts_by_address_list( + context, + &mime_parser.recipients, + to_member_fingerprints, + Some(chat_id), + ) + .await?; + } else { + let ids = match mime_parser.was_encrypted() { + true => { + lookup_pgp_contacts_by_address_list( + context, + &mime_parser.recipients, + to_member_fingerprints, + chat_id, + ) + .await? + } + false => vec![], + }; + if chat_id.is_some() || (mime_parser.was_encrypted() && !ids.contains(&None)) // Prefer creating PGP chats if there are any PGP contacts. At least this prevents // from replying unencrypted. || ids .iter() .any(|&c| c.is_some() && c != Some(ContactId::SELF)) - { - to_ids = ids; - } else { - to_ids = add_or_lookup_contacts_by_address_list( - context, - &mime_parser.recipients, - if !mime_parser.incoming { - Origin::OutgoingTo - } else if incoming_origin.is_known() { - Origin::IncomingTo - } else { - Origin::IncomingUnknownTo - }, - ) - .await?; + { + to_ids = ids; + } else { + to_ids = add_or_lookup_contacts_by_address_list( + context, + &mime_parser.recipients, + if !mime_parser.incoming { + Origin::OutgoingTo + } else if incoming_origin.is_known() { + Origin::IncomingTo + } else { + Origin::IncomingUnknownTo + }, + ) + .await?; + } } - } - past_ids = add_or_lookup_contacts_by_address_list( - context, - &mime_parser.past_members, - Origin::Hidden, - ) - .await?; + past_ids = add_or_lookup_contacts_by_address_list( + context, + &mime_parser.past_members, + Origin::Hidden, + ) + .await?; + } }; let received_msg; From 0c35aa3a2bc5bda4d376305f2e7ba250a9f97079 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 17 May 2025 01:06:41 +0000 Subject: [PATCH 261/381] No need to check for 1 recpient once we decided on 1:1 --- src/receive_imf.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 2fc582a3b5..071156469a 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -544,10 +544,9 @@ pub(crate) async fn receive_imf_inner( .await?; } ChatAssignment::OneOneChat => { - if pgp_to_ids.len() == 1 - && pgp_to_ids - .first() - .is_some_and(|contact_id| contact_id.is_some()) + if pgp_to_ids + .first() + .is_some_and(|contact_id| contact_id.is_some()) { // There is a single recipient and we have // mapped it to a PGP contact. From 3acfbe6fbc97b058b8e80470554c19006e94a03c Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 17 May 2025 01:06:41 +0000 Subject: [PATCH 262/381] pass chat_assignment to add_parts --- src/receive_imf.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 071156469a..bd9c339831 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -665,6 +665,7 @@ pub(crate) async fn receive_imf_inner( prevent_rename, verified_encryption, parent_message, + chat_assignment, ) .await .context("add_parts error")? @@ -992,6 +993,7 @@ async fn add_parts( prevent_rename: bool, verified_encryption: VerifiedEncryption, parent_message: Option, + chat_assignment: ChatAssignment ) -> Result { let is_bot = context.get_config_bool(Config::Bot).await?; let rfc724_mid_orig = &mime_parser From fd1eb7d8ba0b6cfe91e28a7bc1c5c18fe661235d Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 17 May 2025 17:55:21 +0000 Subject: [PATCH 263/381] WIP: use chat assignment in add_parts --- src/receive_imf.rs | 113 ++++++++++++++------------- src/receive_imf/receive_imf_tests.rs | 6 +- 2 files changed, 58 insertions(+), 61 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index bd9c339831..7b4772610e 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -993,7 +993,7 @@ async fn add_parts( prevent_rename: bool, verified_encryption: VerifiedEncryption, parent_message: Option, - chat_assignment: ChatAssignment + chat_assignment: ChatAssignment, ) -> Result { let is_bot = context.get_config_bool(Config::Bot).await?; let rfc724_mid_orig = &mime_parser @@ -1113,25 +1113,62 @@ async fn add_parts( create_blocked_default }; - // Try to assign to a chat based on Chat-Group-ID. if chat_id.is_none() { - if let Some(grpid) = mime_parser.get_chat_group_id().map(|s| s.to_string()) { - if let Some((id, _protected, blocked)) = - chat::get_chat_id_by_grpid(context, &grpid).await? - { - chat_id = Some(id); - chat_id_blocked = blocked; - } else if allow_creation || test_normal_chat.is_some() { - if let Some((new_chat_id, new_chat_id_blocked)) = create_group( + match &chat_assignment { + ChatAssignment::GroupChat { grpid } => { + // Try to assign to a chat based on Chat-Group-ID. + if let Some((id, _protected, blocked)) = + chat::get_chat_id_by_grpid(context, &grpid).await? + { + chat_id = Some(id); + chat_id_blocked = blocked; + } else if allow_creation || test_normal_chat.is_some() { + if let Some((new_chat_id, new_chat_id_blocked)) = create_group( + context, + mime_parser, + is_partial_download.is_some(), + create_blocked, + from_id, + to_ids, + past_ids, + &verified_encryption, + &grpid, + ) + .await? + { + chat_id = Some(new_chat_id); + chat_id_blocked = new_chat_id_blocked; + } + } + } + ChatAssignment::MailingList { .. } => { + if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() { + if let Some((new_chat_id, new_chat_id_blocked)) = + create_or_lookup_mailinglist( + context, + allow_creation, + mailinglist_header, + mime_parser, + ) + .await? + { + chat_id = Some(new_chat_id); + chat_id_blocked = new_chat_id_blocked; + + apply_mailinglist_changes(context, mime_parser, new_chat_id).await?; + } + } + } + ChatAssignment::AdHocGroup | ChatAssignment::OneOneChat => { + if let Some((new_chat_id, new_chat_id_blocked)) = lookup_chat_or_create_adhoc_group( context, mime_parser, - is_partial_download.is_some(), - create_blocked, - from_id, + &parent_message, to_ids, - past_ids, - &verified_encryption, - &grpid, + from_id, + allow_creation || test_normal_chat.is_some(), + create_blocked, + is_partial_download.is_some(), ) .await? { @@ -1142,27 +1179,12 @@ async fn add_parts( } } - if chat_id.is_none() { - if let Some((new_chat_id, new_chat_id_blocked)) = lookup_chat_or_create_adhoc_group( - context, - mime_parser, - &parent_message, - to_ids, - from_id, - allow_creation || test_normal_chat.is_some(), - create_blocked, - is_partial_download.is_some(), - ) - .await? - { - chat_id = Some(new_chat_id); - chat_id_blocked = new_chat_id_blocked; - } - } - // if the chat is somehow blocked but we want to create a non-blocked chat, // unblock the chat - if chat_id_blocked != Blocked::Not && create_blocked != Blocked::Yes { + if chat_id_blocked != Blocked::Not + && create_blocked != Blocked::Yes + && !matches!(chat_assignment, ChatAssignment::MailingList { .. }) + { if let Some(chat_id) = chat_id { chat_id.set_blocked(context, create_blocked).await?; chat_id_blocked = create_blocked; @@ -1206,27 +1228,6 @@ async fn add_parts( .await?; } - if chat_id.is_none() { - // check if the message belongs to a mailing list - if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() { - if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_mailinglist( - context, - allow_creation, - mailinglist_header, - mime_parser, - ) - .await? - { - chat_id = Some(new_chat_id); - chat_id_blocked = new_chat_id_blocked; - } - } - } - - if let Some(chat_id) = chat_id { - apply_mailinglist_changes(context, mime_parser, chat_id).await?; - } - if chat_id.is_none() { // try to create a normal chat let contact = Contact::get_by_id(context, from_id).await?; diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index aff3f43448..7d6a636c55 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -1416,7 +1416,7 @@ async fn test_mailing_list_with_mimepart_footer_signed() { } /// Test that the changes from apply_mailinglist_changes() are also applied -/// if the message is assigned to the chat by In-Reply-To +/// if the message is a reply. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_apply_mailinglist_changes_assigned_by_reply() { let t = TestContext::new_alice().await; @@ -1435,10 +1435,6 @@ async fn test_apply_mailinglist_changes_assigned_by_reply() { t.get_last_msg().await.in_reply_to.unwrap(), "3333@example.org" ); - // `Assigning message to Chat#... as it's a reply to 3333@example.org` - t.evtracker - .get_info_contains("as it's a reply to 3333@example.org") - .await; let chat = Chat::load_from_db(&t, chat_id).await.unwrap(); assert!(!chat.can_send(&t).await.unwrap()); From 546e0eacae8e8246089dfd2be08ba68e4e772607 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 17 May 2025 19:56:12 +0000 Subject: [PATCH 264/381] some ChatAssignment for outgoing messages --- src/receive_imf.rs | 89 +++++++++++++++++++++++----------------------- 1 file changed, 45 insertions(+), 44 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 7b4772610e..7e49d64428 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1160,17 +1160,18 @@ async fn add_parts( } } ChatAssignment::AdHocGroup | ChatAssignment::OneOneChat => { - if let Some((new_chat_id, new_chat_id_blocked)) = lookup_chat_or_create_adhoc_group( - context, - mime_parser, - &parent_message, - to_ids, - from_id, - allow_creation || test_normal_chat.is_some(), - create_blocked, - is_partial_download.is_some(), - ) - .await? + if let Some((new_chat_id, new_chat_id_blocked)) = + lookup_chat_or_create_adhoc_group( + context, + mime_parser, + &parent_message, + to_ids, + from_id, + allow_creation || test_normal_chat.is_some(), + create_blocked, + is_partial_download.is_some(), + ) + .await? { chat_id = Some(new_chat_id); chat_id_blocked = new_chat_id_blocked; @@ -1373,25 +1374,43 @@ async fn add_parts( } } - // Try to assign to a chat based on Chat-Group-ID. if chat_id.is_none() { - if let Some(grpid) = mime_parser.get_chat_group_id().map(|s| s.to_string()) { - if let Some((id, _protected, blocked)) = - chat::get_chat_id_by_grpid(context, &grpid).await? - { - chat_id = Some(id); - chat_id_blocked = blocked; - } else if allow_creation { - if let Some((new_chat_id, new_chat_id_blocked)) = create_group( + match &chat_assignment { + ChatAssignment::GroupChat { grpid } => { + if let Some((id, _protected, blocked)) = + chat::get_chat_id_by_grpid(context, &grpid).await? + { + chat_id = Some(id); + chat_id_blocked = blocked; + } else if allow_creation { + if let Some((new_chat_id, new_chat_id_blocked)) = create_group( + context, + mime_parser, + is_partial_download.is_some(), + Blocked::Not, + from_id, + to_ids, + past_ids, + &verified_encryption, + &grpid, + ) + .await? + { + chat_id = Some(new_chat_id); + chat_id_blocked = new_chat_id_blocked; + } + } + }, + _ => { + if let Some((new_chat_id, new_chat_id_blocked)) = lookup_chat_or_create_adhoc_group( context, mime_parser, - is_partial_download.is_some(), - Blocked::Not, - from_id, + &parent_message, to_ids, - past_ids, - &verified_encryption, - &grpid, + from_id, + allow_creation, + Blocked::Not, + is_partial_download.is_some(), ) .await? { @@ -1402,24 +1421,6 @@ async fn add_parts( } } - if chat_id.is_none() { - if let Some((new_chat_id, new_chat_id_blocked)) = lookup_chat_or_create_adhoc_group( - context, - mime_parser, - &parent_message, - to_ids, - from_id, - allow_creation, - Blocked::Not, - is_partial_download.is_some(), - ) - .await? - { - chat_id = Some(new_chat_id); - chat_id_blocked = new_chat_id_blocked; - } - } - if !to_ids.is_empty() { if chat_id.is_none() && allow_creation { let to_contact = Contact::get_by_id(context, to_id).await?; From 6a5df61a049fb6e09f5e946dccf0e3fe6b6a1d9c Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 20 May 2025 20:35:54 +0000 Subject: [PATCH 265/381] move parent message filtering out of add_parts --- src/receive_imf.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 7e49d64428..501452e817 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -393,7 +393,8 @@ pub(crate) async fn receive_imf_inner( mime_parser.get_header(HeaderDef::References), mime_parser.get_header(HeaderDef::InReplyTo), ) - .await?; + .await? + .filter(|p| Some(p.id) != replace_msg_id); // Decide on the type of chat we assign the message to. // @@ -1009,8 +1010,6 @@ async fn add_parts( better_msg = Some(stock_str::msg_location_enabled_by(context, from_id).await); } - let parent_message = parent_message.filter(|p| Some(p.id) != replace_msg_id); - let is_dc_message = if mime_parser.has_chat_version() { MessengerMessage::Yes } else if let Some(parent_message) = &parent_message { @@ -1400,19 +1399,20 @@ async fn add_parts( chat_id_blocked = new_chat_id_blocked; } } - }, + } _ => { - if let Some((new_chat_id, new_chat_id_blocked)) = lookup_chat_or_create_adhoc_group( - context, - mime_parser, - &parent_message, - to_ids, - from_id, - allow_creation, - Blocked::Not, - is_partial_download.is_some(), - ) - .await? + if let Some((new_chat_id, new_chat_id_blocked)) = + lookup_chat_or_create_adhoc_group( + context, + mime_parser, + &parent_message, + to_ids, + from_id, + allow_creation, + Blocked::Not, + is_partial_download.is_some(), + ) + .await? { chat_id = Some(new_chat_id); chat_id_blocked = new_chat_id_blocked; From 0d3e6f0c8e5c6e41dd9b279d5b15c943e424ad17 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 20 May 2025 20:50:56 +0000 Subject: [PATCH 266/381] trash MDNs early --- src/receive_imf.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 501452e817..d1a75c1ff4 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1001,9 +1001,6 @@ async fn add_parts( .get_rfc724_mid() .unwrap_or(rfc724_mid.to_string()); - let mut chat_id = None; - let mut chat_id_blocked = Blocked::Not; - let mut better_msg = None; let mut group_changes = GroupChangesInfo::default(); if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled { @@ -1028,6 +1025,14 @@ async fn add_parts( let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default(); + let mut chat_id = None; + let mut chat_id_blocked = Blocked::Not; + + if is_mdn { + chat_id = Some(DC_CHAT_ID_TRASH); + info!(context, "Message is an MDN (TRASH).",); + } + let allow_creation; if mime_parser.decrypting_failed { allow_creation = false; @@ -1070,11 +1075,6 @@ async fn add_parts( } } - if chat_id.is_none() && is_mdn { - chat_id = Some(DC_CHAT_ID_TRASH); - info!(context, "Message is an MDN (TRASH).",); - } - if mime_parser.incoming { to_id = ContactId::SELF; From 02bdff17344b189aac980f9782efea7bb1972119 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 20 May 2025 20:55:32 +0000 Subject: [PATCH 267/381] never allow chat creation by MDN --- src/receive_imf.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index d1a75c1ff4..274b526019 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1027,14 +1027,13 @@ async fn add_parts( let mut chat_id = None; let mut chat_id_blocked = Blocked::Not; + let allow_creation; if is_mdn { chat_id = Some(DC_CHAT_ID_TRASH); + allow_creation = false; info!(context, "Message is an MDN (TRASH).",); - } - - let allow_creation; - if mime_parser.decrypting_failed { + } else if mime_parser.decrypting_failed { allow_creation = false; } else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage && is_dc_message == MessengerMessage::No @@ -1049,10 +1048,10 @@ async fn add_parts( allow_creation = false; } ShowEmails::AcceptedContacts => allow_creation = false, - ShowEmails::All => allow_creation = !is_mdn, + ShowEmails::All => allow_creation = true, } } else { - allow_creation = !is_mdn && !is_reaction; + allow_creation = !is_reaction; } // check if the message introduces a new chat: From 7afcd7d499ca11d9314147d575ff785b502e6716 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 20 May 2025 20:56:57 +0000 Subject: [PATCH 268/381] remove outdated comment --- src/receive_imf.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 274b526019..a1d945eae6 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1054,10 +1054,6 @@ async fn add_parts( allow_creation = !is_reaction; } - // check if the message introduces a new chat: - // - outgoing messages introduce a chat with the first to: address if they are sent by a messenger - // - incoming messages introduce a chat only for known contacts if they are sent by a messenger - // (of course, the user can add other chats manually later) let to_id: ContactId; let state: MessageState; let hidden = is_reaction; From 48e840d7c6e10fdd6fa27d64462d70d62851b0f9 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 20 May 2025 21:01:53 +0000 Subject: [PATCH 269/381] trash DSN early --- src/receive_imf.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index a1d945eae6..feb3e1c769 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1032,7 +1032,12 @@ async fn add_parts( if is_mdn { chat_id = Some(DC_CHAT_ID_TRASH); allow_creation = false; - info!(context, "Message is an MDN (TRASH).",); + info!(context, "Message is an MDN (TRASH)."); + } else if mime_parser.delivery_report.is_some() { + chat_id = Some(DC_CHAT_ID_TRASH); + allow_creation = false; + info!(context, "Message is a DSN (TRASH)."); + markseen_on_imap_table(context, rfc724_mid).await.ok(); } else if mime_parser.decrypting_failed { allow_creation = false; } else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage @@ -1075,12 +1080,6 @@ async fn add_parts( let test_normal_chat = ChatIdBlocked::lookup_by_contact(context, from_id).await?; - if chat_id.is_none() && mime_parser.delivery_report.is_some() { - chat_id = Some(DC_CHAT_ID_TRASH); - info!(context, "Message is a DSN (TRASH).",); - markseen_on_imap_table(context, rfc724_mid).await.ok(); - } - let create_blocked_default = if is_bot { Blocked::Not } else { From 200248fdec0e14fe59626b0e99b77f00c986eaa4 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 20 May 2025 21:06:30 +0000 Subject: [PATCH 270/381] trash more messages early --- src/receive_imf.rs | 49 ++++++++++--------- test-data/message/mixed-up-long.eml | 7 --- ...underbird_encrypted_signed_with_pubkey.eml | 7 --- ...pted_unsigned_with_unencrypted_subject.eml | 7 --- .../thunderbird_signed_unencrypted.eml | 7 --- ...thunderbird_with_autocrypt_unencrypted.eml | 7 --- 6 files changed, 25 insertions(+), 59 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index feb3e1c769..3a3c5bf05d 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1025,19 +1025,37 @@ async fn add_parts( let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default(); + let should_trash = if is_mdn { + info!(context, "Message is an MDN (TRASH)."); + true + } else if mime_parser.delivery_report.is_some() { + info!(context, "Message is a DSN (TRASH)."); + markseen_on_imap_table(context, rfc724_mid).await.ok(); + true + } else if mime_parser.sync_items.is_some() { + true + } else if mime_parser + .get_header(HeaderDef::XMozillaDraftInfo) + .is_some() + { + // Mozilla Thunderbird does not set \Draft flag on "Templates", but sets + // X-Mozilla-Draft-Info header, which can be used to detect both drafts and templates + // created by Thunderbird. + + // Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them + info!(context, "Email is probably just a draft (TRASH)."); + true + } else { + false + }; + let mut chat_id = None; let mut chat_id_blocked = Blocked::Not; let allow_creation; - if is_mdn { + if should_trash { chat_id = Some(DC_CHAT_ID_TRASH); allow_creation = false; - info!(context, "Message is an MDN (TRASH)."); - } else if mime_parser.delivery_report.is_some() { - chat_id = Some(DC_CHAT_ID_TRASH); - allow_creation = false; - info!(context, "Message is a DSN (TRASH)."); - markseen_on_imap_table(context, rfc724_mid).await.ok(); } else if mime_parser.decrypting_failed { allow_creation = false; } else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage @@ -1323,23 +1341,6 @@ async fn add_parts( // with only a single `hidden-recipients` group in this case. let self_sent = to_ids.len() <= 1 && to_id == ContactId::SELF; - if mime_parser.sync_items.is_some() && self_sent { - chat_id = Some(DC_CHAT_ID_TRASH); - } - - // Mozilla Thunderbird does not set \Draft flag on "Templates", but sets - // X-Mozilla-Draft-Info header, which can be used to detect both drafts and templates - // created by Thunderbird. - let is_draft = mime_parser - .get_header(HeaderDef::XMozillaDraftInfo) - .is_some(); - - if is_draft { - // Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them - info!(context, "Email is probably just a draft (TRASH)."); - chat_id = Some(DC_CHAT_ID_TRASH); - } - if mime_parser.decrypting_failed { chat_id = Some(DC_CHAT_ID_TRASH); let last_time = context diff --git a/test-data/message/mixed-up-long.eml b/test-data/message/mixed-up-long.eml index df51f1d968..dcadfeeb6d 100644 --- a/test-data/message/mixed-up-long.eml +++ b/test-data/message/mixed-up-long.eml @@ -1,6 +1,3 @@ -From - Tue, 29 Aug 2023 20:24:31 GMT -X-Mozilla-Status: 0801 -X-Mozilla-Status2: 00000000 Message-ID: <05eae88e-35c9-5e5e-405f-11e8a3b44513@example.org> Date: Tue, 29 Aug 2023 17:24:31 -0300 MIME-Version: 1.0 @@ -9,10 +6,6 @@ User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Content-Language: en-US To: bob@example.net From: Alice -X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0; - attachmentreminder=0; deliveryformat=0 -X-Identity-Key: id3 -Fcc: imap://alice%40example.org@in.example.org/Sent Subject: ... Content-Type: multipart/mixed; boundary="------------2IZJ0SaOTFMF25fU1nsH7bxg" diff --git a/test-data/message/thunderbird_encrypted_signed_with_pubkey.eml b/test-data/message/thunderbird_encrypted_signed_with_pubkey.eml index 8eeb6d892e..6acba5422d 100644 --- a/test-data/message/thunderbird_encrypted_signed_with_pubkey.eml +++ b/test-data/message/thunderbird_encrypted_signed_with_pubkey.eml @@ -1,6 +1,3 @@ -From - Thu, 02 Nov 2023 05:20:27 GMT -X-Mozilla-Status: 0801 -X-Mozilla-Status2: 00000000 Message-ID: <956fad6d-206e-67af-2443-3ea5819418ff@example.org> Date: Thu, 2 Nov 2023 02:20:27 -0300 MIME-Version: 1.0 @@ -43,10 +40,6 @@ Autocrypt: addr=alice@example.org; keydata= MGDpk/1NVuMnIHJESRg/SSFV6sElgq38k9wAT2oUqLcYvYI07nHmnuciaGygkCcGt+l2PvAa j4mkQQvMU0cNRDBybk5aKi820oGIJjT7e+5RnD2mYZQdOAbQhDVCHvrfS1I60bsHT1MHqyAa /qMLjKwBpKEd/w== -X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0; - attachmentreminder=0; deliveryformat=0 -X-Identity-Key: id3 -Fcc: imap://alice%40example.org@in.example.org/Sent Subject: ... Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; diff --git a/test-data/message/thunderbird_encrypted_unsigned_with_unencrypted_subject.eml b/test-data/message/thunderbird_encrypted_unsigned_with_unencrypted_subject.eml index 2375e7ade6..c43658e28e 100644 --- a/test-data/message/thunderbird_encrypted_unsigned_with_unencrypted_subject.eml +++ b/test-data/message/thunderbird_encrypted_unsigned_with_unencrypted_subject.eml @@ -1,6 +1,3 @@ -From - Sun, 19 Nov 2023 01:08:24 GMT -X-Mozilla-Status: 0800 -X-Mozilla-Status2: 00000000 Message-ID: <38a2a29b-8261-403b-abb5-56b0a87d2ff4@example.org> Date: Sat, 18 Nov 2023 22:08:23 -0300 MIME-Version: 1.0 @@ -42,10 +39,6 @@ Autocrypt: addr=alice@example.org; keydata= MGDpk/1NVuMnIHJESRg/SSFV6sElgq38k9wAT2oUqLcYvYI07nHmnuciaGygkCcGt+l2PvAa j4mkQQvMU0cNRDBybk5aKi820oGIJjT7e+5RnD2mYZQdOAbQhDVCHvrfS1I60bsHT1MHqyAa /qMLjKwBpKEd/w== -X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0; - attachmentreminder=0; deliveryformat=0 -X-Identity-Key: id3 -Fcc: imap://alice%40example.org@in.example.org/Sent Subject: Hello! Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; diff --git a/test-data/message/thunderbird_signed_unencrypted.eml b/test-data/message/thunderbird_signed_unencrypted.eml index ec9646050b..f0338abb98 100644 --- a/test-data/message/thunderbird_signed_unencrypted.eml +++ b/test-data/message/thunderbird_signed_unencrypted.eml @@ -1,6 +1,3 @@ -From - Thu, 15 Dec 2022 14:45:17 GMT -X-Mozilla-Status: 0801 -X-Mozilla-Status2: 00000000 Message-ID: Date: Thu, 15 Dec 2022 11:45:16 -0300 MIME-Version: 1.0 @@ -10,10 +7,6 @@ Content-Language: en-US To: bob@example.net From: Alice Subject: test message 15:53 -X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0; - attachmentreminder=0; deliveryformat=0 -X-Identity-Key: id3 -Fcc: imap://alice%40example.org@in.example.org/Sent Content-Type: multipart/signed; micalg=pgp-sha256; protocol="application/pgp-signature"; boundary="------------iX39J1p7DOgblwacjo0e7jX7" diff --git a/test-data/message/thunderbird_with_autocrypt_unencrypted.eml b/test-data/message/thunderbird_with_autocrypt_unencrypted.eml index 201044751a..a3c3a5389f 100644 --- a/test-data/message/thunderbird_with_autocrypt_unencrypted.eml +++ b/test-data/message/thunderbird_with_autocrypt_unencrypted.eml @@ -1,6 +1,3 @@ -From - Wed, 14 Dec 2022 18:53:03 GMT -X-Mozilla-Status: 0801 -X-Mozilla-Status2: 00000000 Message-ID: <87d75c7e-0f52-1335-e437-af605c09f954@example.org> Date: Wed, 14 Dec 2022 15:53:03 -0300 MIME-Version: 1.0 @@ -44,10 +41,6 @@ Autocrypt: addr=alice@example.org; keydata= MGDpk/1NVuMnIHJESRg/SSFV6sElgq38k9wAT2oUqLcYvYI07nHmnuciaGygkCcGt+l2PvAa j4mkQQvMU0cNRDBybk5aKi820oGIJjT7e+5RnD2mYZQdOAbQhDVCHvrfS1I60bsHT1MHqyAa /qMLjKwBpKEd/w== -X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0; - attachmentreminder=0; deliveryformat=0 -X-Identity-Key: id3 -Fcc: imap://alice%40example.org@in.example.org/Sent Content-Type: multipart/signed; micalg=pgp-sha256; protocol="application/pgp-signature"; boundary="------------x6XEHrf0vHmVgEo6f9bMGGUy" From fb78402e8d7b12d0d800dfa4354fec09c002c3b6 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 20 May 2025 21:29:42 +0000 Subject: [PATCH 271/381] trash webxdc status updates early --- src/receive_imf.rs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 3a3c5bf05d..dfe5bb9f20 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1045,6 +1045,18 @@ async fn add_parts( // Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them info!(context, "Email is probably just a draft (TRASH)."); true + } else if mime_parser.webxdc_status_update.is_some() && mime_parser.parts.len() == 1 { + if let Some(part) = mime_parser.parts.first() { + if part.typ == Viewtype::Text && part.msg.is_empty() { + info!(context, "Message is a status update only (TRASH)."); + markseen_on_imap_table(context, rfc724_mid).await.ok(); + true + } else { + false + } + } else { + false + } } else { false }; @@ -1077,6 +1089,7 @@ async fn add_parts( allow_creation = !is_reaction; } + let to_id: ContactId; let state: MessageState; let hidden = is_reaction; @@ -1493,16 +1506,6 @@ async fn add_parts( } } - if mime_parser.webxdc_status_update.is_some() && mime_parser.parts.len() == 1 { - if let Some(part) = mime_parser.parts.first() { - if part.typ == Viewtype::Text && part.msg.is_empty() { - chat_id = Some(DC_CHAT_ID_TRASH); - info!(context, "Message is a status update only (TRASH)."); - markseen_on_imap_table(context, rfc724_mid).await.ok(); - } - } - } - let orig_chat_id = chat_id; let mut chat_id = chat_id.unwrap_or_else(|| { info!(context, "No chat id for message (TRASH)."); From dfe9d7ed79af7c87e70347373444804fbe4b735d Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 20 May 2025 23:06:50 +0000 Subject: [PATCH 272/381] trash outgoing undecryptable messages early --- src/receive_imf.rs | 54 ++++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index dfe5bb9f20..52a4746595 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1034,6 +1034,32 @@ async fn add_parts( true } else if mime_parser.sync_items.is_some() { true + } else if mime_parser.decrypting_failed && !mime_parser.incoming { + // Outgoing undecryptable message. + let last_time = context + .get_config_i64(Config::LastCantDecryptOutgoingMsgs) + .await?; + let now = tools::time(); + let update_config = if last_time.saturating_add(24 * 60 * 60) <= now { + let mut msg = + Message::new_text(stock_str::cant_decrypt_outgoing_msgs(context).await); + chat::add_device_msg(context, None, Some(&mut msg)) + .await + .log_err(context) + .ok(); + true + } else { + last_time > now + }; + if update_config { + context + .set_config_internal( + Config::LastCantDecryptOutgoingMsgs, + Some(&now.to_string()), + ) + .await?; + } + true } else if mime_parser .get_header(HeaderDef::XMozillaDraftInfo) .is_some() @@ -1089,7 +1115,6 @@ async fn add_parts( allow_creation = !is_reaction; } - let to_id: ContactId; let state: MessageState; let hidden = is_reaction; @@ -1354,33 +1379,6 @@ async fn add_parts( // with only a single `hidden-recipients` group in this case. let self_sent = to_ids.len() <= 1 && to_id == ContactId::SELF; - if mime_parser.decrypting_failed { - chat_id = Some(DC_CHAT_ID_TRASH); - let last_time = context - .get_config_i64(Config::LastCantDecryptOutgoingMsgs) - .await?; - let now = tools::time(); - let update_config = if last_time.saturating_add(24 * 60 * 60) <= now { - let mut msg = - Message::new_text(stock_str::cant_decrypt_outgoing_msgs(context).await); - chat::add_device_msg(context, None, Some(&mut msg)) - .await - .log_err(context) - .ok(); - true - } else { - last_time > now - }; - if update_config { - context - .set_config_internal( - Config::LastCantDecryptOutgoingMsgs, - Some(&now.to_string()), - ) - .await?; - } - } - if chat_id.is_none() { match &chat_assignment { ChatAssignment::GroupChat { grpid } => { From 1073b0b60001257122f9937a3cc89a84d779bbc5 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 20 May 2025 23:34:13 +0000 Subject: [PATCH 273/381] doc comment --- src/receive_imf.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 52a4746595..2c9087058c 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -82,12 +82,12 @@ pub struct ReceivedMsg { /// group after looking up PGP-contacts /// or vice versa. enum ChatAssignment { - // Group chat with a Group ID. - // - // Lookup PGP contacts and - // assign to encrypted group - // even if the message itself - // is not encrypted. + /// Group chat with a Group ID. + /// + /// Lookup PGP contacts and + /// assign to encrypted group + /// even if the message itself + /// is not encrypted. GroupChat { grpid: String, }, From b8fe6577cc0fe3f032001873c2b268a0e61d88a1 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 20 May 2025 23:47:55 +0000 Subject: [PATCH 274/381] ChatAssignment::Trash --- src/receive_imf.rs | 141 ++++++++++++++------------- src/receive_imf/receive_imf_tests.rs | 14 ++- 2 files changed, 78 insertions(+), 77 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 2c9087058c..fe49395471 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -82,15 +82,16 @@ pub struct ReceivedMsg { /// group after looking up PGP-contacts /// or vice versa. enum ChatAssignment { + /// Trash the message. + Trash, + /// Group chat with a Group ID. /// /// Lookup PGP contacts and /// assign to encrypted group /// even if the message itself /// is not encrypted. - GroupChat { - grpid: String, - }, + GroupChat { grpid: String }, /// Mailing list or broadcast list. /// @@ -106,9 +107,7 @@ enum ChatAssignment { /// up except the `from_id` /// which may be an email address contact /// or a PGP-contact. - MailingList { - listid: String, - }, + MailingList { listid: String }, /// Group chat without a Group ID. /// @@ -396,11 +395,71 @@ pub(crate) async fn receive_imf_inner( .await? .filter(|p| Some(p.id) != replace_msg_id); + let should_trash = if !mime_parser.mdn_reports.is_empty() { + info!(context, "Message is an MDN (TRASH)."); + true + } else if mime_parser.delivery_report.is_some() { + info!(context, "Message is a DSN (TRASH)."); + markseen_on_imap_table(context, rfc724_mid).await.ok(); + true + } else if mime_parser.sync_items.is_some() { + true + } else if mime_parser.decrypting_failed && !mime_parser.incoming { + // Outgoing undecryptable message. + let last_time = context + .get_config_i64(Config::LastCantDecryptOutgoingMsgs) + .await?; + let now = tools::time(); + let update_config = if last_time.saturating_add(24 * 60 * 60) <= now { + let mut msg = Message::new_text(stock_str::cant_decrypt_outgoing_msgs(context).await); + chat::add_device_msg(context, None, Some(&mut msg)) + .await + .log_err(context) + .ok(); + true + } else { + last_time > now + }; + if update_config { + context + .set_config_internal(Config::LastCantDecryptOutgoingMsgs, Some(&now.to_string())) + .await?; + } + true + } else if mime_parser + .get_header(HeaderDef::XMozillaDraftInfo) + .is_some() + { + // Mozilla Thunderbird does not set \Draft flag on "Templates", but sets + // X-Mozilla-Draft-Info header, which can be used to detect both drafts and templates + // created by Thunderbird. + + // Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them + info!(context, "Email is probably just a draft (TRASH)."); + true + } else if mime_parser.webxdc_status_update.is_some() && mime_parser.parts.len() == 1 { + if let Some(part) = mime_parser.parts.first() { + if part.typ == Viewtype::Text && part.msg.is_empty() { + info!(context, "Message is a status update only (TRASH)."); + markseen_on_imap_table(context, rfc724_mid).await.ok(); + true + } else { + false + } + } else { + false + } + } else { + false + }; + // Decide on the type of chat we assign the message to. // // The chat may not exist yet, i.e. there may be // no database row and ChatId yet. - let chat_assignment = if let Some(grpid) = mime_parser.get_chat_group_id() { + let chat_assignment = if should_trash { + ChatAssignment::Trash + } else if let Some(grpid) = mime_parser.get_chat_group_id() { if mime_parser.was_encrypted() { ChatAssignment::GroupChat { grpid: grpid.to_string(), @@ -431,6 +490,7 @@ pub(crate) async fn receive_imf_inner( // lookup the address of receipient in the list of addresses used in the group, // but want to assign the message to 1:1 chat. let chat_id = match chat_assignment { + ChatAssignment::Trash => None, ChatAssignment::GroupChat { ref grpid } => { if let Some((chat_id, _protected, _blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? @@ -519,7 +579,7 @@ pub(crate) async fn receive_imf_inner( .await?; } } - ChatAssignment::MailingList { .. } => { + ChatAssignment::Trash | ChatAssignment::MailingList { .. } => { to_ids = Vec::new(); past_ids = Vec::new(); } @@ -1025,67 +1085,7 @@ async fn add_parts( let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default(); - let should_trash = if is_mdn { - info!(context, "Message is an MDN (TRASH)."); - true - } else if mime_parser.delivery_report.is_some() { - info!(context, "Message is a DSN (TRASH)."); - markseen_on_imap_table(context, rfc724_mid).await.ok(); - true - } else if mime_parser.sync_items.is_some() { - true - } else if mime_parser.decrypting_failed && !mime_parser.incoming { - // Outgoing undecryptable message. - let last_time = context - .get_config_i64(Config::LastCantDecryptOutgoingMsgs) - .await?; - let now = tools::time(); - let update_config = if last_time.saturating_add(24 * 60 * 60) <= now { - let mut msg = - Message::new_text(stock_str::cant_decrypt_outgoing_msgs(context).await); - chat::add_device_msg(context, None, Some(&mut msg)) - .await - .log_err(context) - .ok(); - true - } else { - last_time > now - }; - if update_config { - context - .set_config_internal( - Config::LastCantDecryptOutgoingMsgs, - Some(&now.to_string()), - ) - .await?; - } - true - } else if mime_parser - .get_header(HeaderDef::XMozillaDraftInfo) - .is_some() - { - // Mozilla Thunderbird does not set \Draft flag on "Templates", but sets - // X-Mozilla-Draft-Info header, which can be used to detect both drafts and templates - // created by Thunderbird. - - // Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them - info!(context, "Email is probably just a draft (TRASH)."); - true - } else if mime_parser.webxdc_status_update.is_some() && mime_parser.parts.len() == 1 { - if let Some(part) = mime_parser.parts.first() { - if part.typ == Viewtype::Text && part.msg.is_empty() { - info!(context, "Message is a status update only (TRASH)."); - markseen_on_imap_table(context, rfc724_mid).await.ok(); - true - } else { - false - } - } else { - false - } - } else { - false - }; + let should_trash = matches!(chat_assignment, ChatAssignment::Trash); let mut chat_id = None; let mut chat_id_blocked = Blocked::Not; @@ -1164,6 +1164,9 @@ async fn add_parts( if chat_id.is_none() { match &chat_assignment { + ChatAssignment::Trash => { + chat_id = Some(DC_CHAT_ID_TRASH); + } ChatAssignment::GroupChat { grpid } => { // Try to assign to a chat based on Chat-Group-ID. if let Some((id, _protected, blocked)) = diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 7d6a636c55..47e04f046a 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -3219,12 +3219,10 @@ async fn test_outgoing_undecryptable() -> Result<()> { let raw = include_bytes!("../../test-data/message/thunderbird_with_autocrypt.eml"); receive_imf(alice, raw, false).await?; + // Undecryptable message does not even create a contact. let bob_contact_id = Contact::lookup_id_by_addr(alice, "bob@example.net", Origin::OutgoingTo) - .await? - .unwrap(); - assert!(ChatId::lookup_by_contact(alice, bob_contact_id) - .await? - .is_none()); + .await?; + assert!(bob_contact_id.is_none()); let dev_chat_id = ChatId::lookup_by_contact(alice, ContactId::DEVICE) .await? @@ -3238,9 +3236,9 @@ async fn test_outgoing_undecryptable() -> Result<()> { let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_signed.eml"); receive_imf(alice, raw, false).await?; - assert!(ChatId::lookup_by_contact(alice, bob_contact_id) - .await? - .is_none()); + let bob_contact_id = Contact::lookup_id_by_addr(alice, "bob@example.net", Origin::OutgoingTo) + .await?; + assert!(bob_contact_id.is_none()); // The device message mustn't be added too frequently. assert_eq!(alice.get_last_msg_in(dev_chat_id).await.id, dev_msg.id); From 75ce32a95a06e6817c7a31940d3269c68c03f596 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 21 May 2025 00:49:45 +0000 Subject: [PATCH 275/381] rustfmt --- src/receive_imf/receive_imf_tests.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 47e04f046a..1f1f564356 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -3220,8 +3220,8 @@ async fn test_outgoing_undecryptable() -> Result<()> { receive_imf(alice, raw, false).await?; // Undecryptable message does not even create a contact. - let bob_contact_id = Contact::lookup_id_by_addr(alice, "bob@example.net", Origin::OutgoingTo) - .await?; + let bob_contact_id = + Contact::lookup_id_by_addr(alice, "bob@example.net", Origin::OutgoingTo).await?; assert!(bob_contact_id.is_none()); let dev_chat_id = ChatId::lookup_by_contact(alice, ContactId::DEVICE) @@ -3236,8 +3236,8 @@ async fn test_outgoing_undecryptable() -> Result<()> { let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_signed.eml"); receive_imf(alice, raw, false).await?; - let bob_contact_id = Contact::lookup_id_by_addr(alice, "bob@example.net", Origin::OutgoingTo) - .await?; + let bob_contact_id = + Contact::lookup_id_by_addr(alice, "bob@example.net", Origin::OutgoingTo).await?; assert!(bob_contact_id.is_none()); // The device message mustn't be added too frequently. assert_eq!(alice.get_last_msg_in(dev_chat_id).await.id, dev_msg.id); From eda5f563745af240b10e81d5bd5335fb35634ad3 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 21 May 2025 01:07:23 +0000 Subject: [PATCH 276/381] simplify allow_creation --- src/receive_imf.rs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index fe49395471..c1b0b08dcd 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1085,17 +1085,14 @@ async fn add_parts( let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default(); - let should_trash = matches!(chat_assignment, ChatAssignment::Trash); - let mut chat_id = None; let mut chat_id_blocked = Blocked::Not; - let allow_creation; - if should_trash { + let allow_creation = if matches!(chat_assignment, ChatAssignment::Trash) { chat_id = Some(DC_CHAT_ID_TRASH); - allow_creation = false; + false } else if mime_parser.decrypting_failed { - allow_creation = false; + false } else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage && is_dc_message == MessengerMessage::No && !context.get_config_bool(Config::IsChatmail).await? @@ -1106,14 +1103,14 @@ async fn add_parts( ShowEmails::Off => { info!(context, "Classical email not shown (TRASH)."); chat_id = Some(DC_CHAT_ID_TRASH); - allow_creation = false; + false } - ShowEmails::AcceptedContacts => allow_creation = false, - ShowEmails::All => allow_creation = true, + ShowEmails::AcceptedContacts => false, + ShowEmails::All => true, } } else { - allow_creation = !is_reaction; - } + !is_reaction + }; let to_id: ContactId; let state: MessageState; From 15ba0a1d4e7997579bd9d58b2b5316184b05005f Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 21 May 2025 01:11:15 +0000 Subject: [PATCH 277/381] move trash chat assignment --- src/receive_imf.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index c1b0b08dcd..a5e40899d4 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1089,7 +1089,6 @@ async fn add_parts( let mut chat_id_blocked = Blocked::Not; let allow_creation = if matches!(chat_assignment, ChatAssignment::Trash) { - chat_id = Some(DC_CHAT_ID_TRASH); false } else if mime_parser.decrypting_failed { false @@ -1381,6 +1380,9 @@ async fn add_parts( if chat_id.is_none() { match &chat_assignment { + ChatAssignment::Trash => { + chat_id = Some(DC_CHAT_ID_TRASH); + } ChatAssignment::GroupChat { grpid } => { if let Some((id, _protected, blocked)) = chat::get_chat_id_by_grpid(context, &grpid).await? From 91ae9d8a44c3d9b562bb329263e9e0c047051b90 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 21 May 2025 01:14:20 +0000 Subject: [PATCH 278/381] don't disallow trash chat creation --- src/receive_imf.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index a5e40899d4..a551f04326 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1088,9 +1088,7 @@ async fn add_parts( let mut chat_id = None; let mut chat_id_blocked = Blocked::Not; - let allow_creation = if matches!(chat_assignment, ChatAssignment::Trash) { - false - } else if mime_parser.decrypting_failed { + let allow_creation = if mime_parser.decrypting_failed { false } else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage && is_dc_message == MessengerMessage::No From cc8bc702ec5b7c2552aa124b80fe986dbb1b7661 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 21 May 2025 01:24:07 +0000 Subject: [PATCH 279/381] clippy --- src/receive_imf.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index a551f04326..83dad69b7e 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -510,11 +510,7 @@ pub(crate) async fn receive_imf_inner( ChatAssignment::MailingList { .. } => None, ChatAssignment::OneOneChat => { if is_partial_download.is_none() && !mime_parser.incoming { - if let Some(parent_message) = &parent_message { - Some(parent_message.chat_id) - } else { - None - } + parent_message.as_ref().map(|m| m.chat_id) } else { None } @@ -1164,7 +1160,7 @@ async fn add_parts( ChatAssignment::GroupChat { grpid } => { // Try to assign to a chat based on Chat-Group-ID. if let Some((id, _protected, blocked)) = - chat::get_chat_id_by_grpid(context, &grpid).await? + chat::get_chat_id_by_grpid(context, grpid).await? { chat_id = Some(id); chat_id_blocked = blocked; @@ -1397,7 +1393,7 @@ async fn add_parts( to_ids, past_ids, &verified_encryption, - &grpid, + grpid, ) .await? { From d5b70ca5794b2bcf8cf44fe47af4db1fcd2dee92 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 21 May 2025 01:31:14 +0000 Subject: [PATCH 280/381] unused field --- src/receive_imf.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 83dad69b7e..30917e6546 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -107,7 +107,7 @@ enum ChatAssignment { /// up except the `from_id` /// which may be an email address contact /// or a PGP-contact. - MailingList { listid: String }, + MailingList, /// Group chat without a Group ID. /// @@ -475,8 +475,8 @@ pub(crate) async fn receive_imf_inner( ChatAssignment::AdHocGroup } } else if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() { - let listid = mailinglist_header_listid(mailinglist_header)?; - ChatAssignment::MailingList { listid } + let _listid = mailinglist_header_listid(mailinglist_header)?; + ChatAssignment::MailingList } else if mime_parser.recipients.len() == 1 { ChatAssignment::OneOneChat } else { From 2cd6d3f0bf08914b9b4fab2117f2dd036a5503cb Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 21 May 2025 02:17:24 +0000 Subject: [PATCH 281/381] clippy --- src/receive_imf.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 30917e6546..5aab7efc67 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -507,7 +507,7 @@ pub(crate) async fn receive_imf_inner( // to lookup PGP-contacts. None } - ChatAssignment::MailingList { .. } => None, + ChatAssignment::MailingList => None, ChatAssignment::OneOneChat => { if is_partial_download.is_none() && !mime_parser.incoming { parent_message.as_ref().map(|m| m.chat_id) @@ -575,7 +575,7 @@ pub(crate) async fn receive_imf_inner( .await?; } } - ChatAssignment::Trash | ChatAssignment::MailingList { .. } => { + ChatAssignment::Trash | ChatAssignment::MailingList => { to_ids = Vec::new(); past_ids = Vec::new(); } @@ -1174,7 +1174,7 @@ async fn add_parts( to_ids, past_ids, &verified_encryption, - &grpid, + grpid, ) .await? { @@ -1183,7 +1183,7 @@ async fn add_parts( } } } - ChatAssignment::MailingList { .. } => { + ChatAssignment::MailingList => { if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() { if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_mailinglist( @@ -1226,7 +1226,7 @@ async fn add_parts( // unblock the chat if chat_id_blocked != Blocked::Not && create_blocked != Blocked::Yes - && !matches!(chat_assignment, ChatAssignment::MailingList { .. }) + && !matches!(chat_assignment, ChatAssignment::MailingList) { if let Some(chat_id) = chat_id { chat_id.set_blocked(context, create_blocked).await?; @@ -1379,7 +1379,7 @@ async fn add_parts( } ChatAssignment::GroupChat { grpid } => { if let Some((id, _protected, blocked)) = - chat::get_chat_id_by_grpid(context, &grpid).await? + chat::get_chat_id_by_grpid(context, grpid).await? { chat_id = Some(id); chat_id_blocked = blocked; From 2dd96f8bc1da7c0acf16c82dfd2465df4edaa922 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 22 May 2025 20:18:54 +0000 Subject: [PATCH 282/381] is_probably_private_reply not called for messages with group ID --- src/receive_imf.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 5aab7efc67..36f6840025 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -2337,6 +2337,11 @@ async fn is_probably_private_reply( mime_parser: &MimeMessage, parent_chat_id: ChatId, ) -> Result { + // Message cannot be a private reply if it has an explicit Chat-Group-ID header. + // + // This function should not even be called in this case. + debug_assert!(mime_parser.get_chat_group_id().is_none()); + // Usually we don't want to show private replies in the parent chat, but in the // 1:1 chat with the sender. // @@ -2350,11 +2355,6 @@ async fn is_probably_private_reply( return Ok(false); } - // Message cannot be a private reply if it has an explicit Chat-Group-ID header. - if mime_parser.get_chat_group_id().is_some() { - return Ok(false); - } - if !mime_parser.has_chat_version() { let chat_contacts = chat::get_chat_contacts(context, parent_chat_id).await?; if chat_contacts.len() == 2 && chat_contacts.contains(&ContactId::SELF) { From 9cb5e09636c74f371d9f8256dbb5e495ef10d1c8 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 22 May 2025 20:58:13 +0000 Subject: [PATCH 283/381] encrypt test_send_reaction_multidevice --- src/reaction.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/reaction.rs b/src/reaction.rs index 985bdc15d2..82406f2551 100644 --- a/src/reaction.rs +++ b/src/reaction.rs @@ -980,10 +980,11 @@ Here's my footer -- bob@example.net" #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_send_reaction_multidevice() -> Result<()> { - let alice0 = TestContext::new_alice().await; - let alice1 = TestContext::new_alice().await; - let bob_id = Contact::create(&alice0, "", "bob@example.net").await?; - let chat_id = ChatId::create_for_contact(&alice0, bob_id).await?; + let mut tcm = TestContextManager::new(); + let alice0 = tcm.alice().await; + let alice1 = tcm.alice().await; + let bob = tcm.bob().await; + let chat_id = alice0.create_chat(&bob).await.id; let alice0_msg_id = send_text_msg(&alice0, chat_id, "foo".to_string()).await?; let alice1_msg = alice1.recv_msg(&alice0.pop_sent_msg().await).await; From e9f198d7674ae38fd0cdc0b83c6e68539fc23d3b Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 22 May 2025 20:58:13 +0000 Subject: [PATCH 284/381] less contact IDs --- src/receive_imf.rs | 14 ++++---------- src/tests/verified_chats.rs | 6 +----- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 36f6840025..fb61749a29 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -2182,7 +2182,6 @@ async fn lookup_chat_by_reply( mime_parser: &MimeMessage, parent: &Option, to_ids: &[ContactId], - from_id: ContactId, ) -> Result> { // Try to assign message to the same chat as the parent message. @@ -2196,7 +2195,7 @@ async fn lookup_chat_by_reply( // If this was a private message just to self, it was probably a private reply. // It should not go into the group then, but into the private chat. - if is_probably_private_reply(context, to_ids, from_id, mime_parser, parent_chat.id).await? { + if is_probably_private_reply(context, mime_parser, parent_chat.id).await? { return Ok(None); } @@ -2238,7 +2237,7 @@ async fn lookup_chat_or_create_adhoc_group( if let Some((new_chat_id, new_chat_id_blocked)) = // Try to assign to a chat based on In-Reply-To/References. - lookup_chat_by_reply(context, mime_parser, parent, &to_ids, from_id).await? + lookup_chat_by_reply(context, mime_parser, parent, &to_ids).await? { return Ok(Some((new_chat_id, new_chat_id_blocked))); } @@ -2332,8 +2331,6 @@ async fn lookup_chat_or_create_adhoc_group( /// If it returns false, it shall be assigned to the parent chat. async fn is_probably_private_reply( context: &Context, - to_ids: &[ContactId], - from_id: ContactId, mime_parser: &MimeMessage, parent_chat_id: ChatId, ) -> Result { @@ -2349,9 +2346,7 @@ async fn is_probably_private_reply( // should be assigned to the group chat. We restrict this exception to classical emails, as chat-group-messages // contain a Chat-Group-Id header and can be sorted into the correct chat this way. - let private_message = - (to_ids == [ContactId::SELF]) || (from_id == ContactId::SELF && to_ids.len() == 1); - if !private_message { + if mime_parser.recipients.len() != 1 { return Ok(false); } @@ -2390,8 +2385,7 @@ async fn create_group( // they belong to the group because of the Chat-Group-Id or Message-Id header if let Some(chat_id) = chat_id { if !mime_parser.has_chat_version() - && is_probably_private_reply(context, &to_ids_flat, from_id, mime_parser, chat_id) - .await? + && is_probably_private_reply(context, mime_parser, chat_id).await? { return Ok(None); } diff --git a/src/tests/verified_chats.rs b/src/tests/verified_chats.rs index 04bd2d9c6b..ae9635e961 100644 --- a/src/tests/verified_chats.rs +++ b/src/tests/verified_chats.rs @@ -479,11 +479,7 @@ async fn test_reply() -> Result<()> { let unencrypted_msg = Message::load_from_db(&alice, unencrypted_msg.msg_ids[0]).await?; assert_eq!(unencrypted_msg.text, "Weird reply"); - if verified { - assert_ne!(unencrypted_msg.chat_id, encrypted_msg.chat_id); - } else { - assert_eq!(unencrypted_msg.chat_id, encrypted_msg.chat_id); - } + assert_ne!(unencrypted_msg.chat_id, encrypted_msg.chat_id); } Ok(()) From c3db92bbf566a63917c55a17da581397b7a2d16e Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 22 May 2025 21:20:22 +0000 Subject: [PATCH 285/381] simplify lookup_chat_by_reply --- src/receive_imf.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index fb61749a29..4e8e29866b 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -2199,10 +2199,10 @@ async fn lookup_chat_by_reply( return Ok(None); } - // If the parent chat is a 1:1 chat, and the sender is a classical MUA and added + // If the parent chat is a 1:1 chat, and the sender added // a new person to TO/CC, then the message should not go to the 1:1 chat, but to a // newly created ad-hoc group. - if parent_chat.typ == Chattype::Single && !mime_parser.has_chat_version() && to_ids.len() > 1 { + if parent_chat.typ == Chattype::Single && mime_parser.recipients.len() > 1 { let mut chat_contacts = chat::get_chat_contacts(context, parent_chat.id).await?; chat_contacts.push(ContactId::SELF); if to_ids.iter().any(|id| !chat_contacts.contains(id)) { From 7dc17543820cdb772b5a4769e2557bc4cb88d6a5 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 22 May 2025 21:23:25 +0000 Subject: [PATCH 286/381] remove special case for alias support --- src/receive_imf.rs | 9 ++------- src/receive_imf/receive_imf_tests.rs | 16 ++++++++++------ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 4e8e29866b..fcfe204321 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -2181,7 +2181,6 @@ async fn lookup_chat_by_reply( context: &Context, mime_parser: &MimeMessage, parent: &Option, - to_ids: &[ContactId], ) -> Result> { // Try to assign message to the same chat as the parent message. @@ -2203,11 +2202,7 @@ async fn lookup_chat_by_reply( // a new person to TO/CC, then the message should not go to the 1:1 chat, but to a // newly created ad-hoc group. if parent_chat.typ == Chattype::Single && mime_parser.recipients.len() > 1 { - let mut chat_contacts = chat::get_chat_contacts(context, parent_chat.id).await?; - chat_contacts.push(ContactId::SELF); - if to_ids.iter().any(|id| !chat_contacts.contains(id)) { - return Ok(None); - } + return Ok(None); } // Do not assign unencrypted messages to encrypted chats. @@ -2237,7 +2232,7 @@ async fn lookup_chat_or_create_adhoc_group( if let Some((new_chat_id, new_chat_id_blocked)) = // Try to assign to a chat based on In-Reply-To/References. - lookup_chat_by_reply(context, mime_parser, parent, &to_ids).await? + lookup_chat_by_reply(context, mime_parser, parent).await? { return Ok(Some((new_chat_id, new_chat_id_blocked))); } diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 1f1f564356..f1f3760980 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -1832,17 +1832,21 @@ async fn check_alias_reply(from_dc: bool, chat_request: bool, group_request: boo "bob@example.net" ); // Bob is not part of the group, so override-sender-name should be set - // Check that Claire also gets the message in the same chat. + // Claire gets the reply as ad hoc group. let request = claire.get_last_msg().await; receive_imf(&claire, reply.as_bytes(), false).await.unwrap(); let answer = claire.get_last_msg().await; assert_eq!(answer.get_subject(), "Re: i have a question"); assert!(answer.get_text().contains("the version is 1.0")); - assert_eq!(answer.chat_id, request.chat_id); - assert_eq!( - answer.get_override_sender_name().unwrap(), - "bob@example.net" - ); + if group_request { + assert_eq!(answer.chat_id, request.chat_id); + assert_eq!( + answer.get_override_sender_name().unwrap(), + "bob@example.net" + ); + } else { + assert_ne!(answer.chat_id, request.chat_id); + } } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] From aef2029e0f004e9a024f20350d56ad627ae584b7 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 23 May 2025 00:48:21 +0000 Subject: [PATCH 287/381] pass non-optional message into lookup_chat_by_reply --- src/receive_imf.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index fcfe204321..2f5f181a7c 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -2180,13 +2180,10 @@ async fn save_locations( async fn lookup_chat_by_reply( context: &Context, mime_parser: &MimeMessage, - parent: &Option, + parent: &Message, ) -> Result> { // Try to assign message to the same chat as the parent message. - let Some(parent) = parent else { - return Ok(None); - }; let Some(parent_chat_id) = ChatId::lookup_by_message(parent) else { return Ok(None); }; @@ -2230,11 +2227,13 @@ async fn lookup_chat_or_create_adhoc_group( ) -> Result> { let to_ids: Vec = to_ids.iter().filter_map(|x| *x).collect(); - if let Some((new_chat_id, new_chat_id_blocked)) = - // Try to assign to a chat based on In-Reply-To/References. - lookup_chat_by_reply(context, mime_parser, parent).await? - { - return Ok(Some((new_chat_id, new_chat_id_blocked))); + if let Some(parent) = parent { + if let Some((new_chat_id, new_chat_id_blocked)) = + // Try to assign to a chat based on In-Reply-To/References. + lookup_chat_by_reply(context, mime_parser, parent).await? + { + return Ok(Some((new_chat_id, new_chat_id_blocked))); + } } // Partial download may be an encrypted message with protected Subject header. We do not want to // create a group with "..." or "Encrypted message" as a subject. The same is for undecipherable From 324204b8c579e868705827c9eab0c295e3be37dd Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 23 May 2025 01:01:33 +0000 Subject: [PATCH 288/381] remove some dead code --- src/receive_imf.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 2f5f181a7c..d2ef6aab6f 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -2375,16 +2375,6 @@ async fn create_group( let mut chat_id = None; let mut chat_id_blocked = Default::default(); - // For chat messages, we don't have to guess (is_*probably*_private_reply()) but we know for sure that - // they belong to the group because of the Chat-Group-Id or Message-Id header - if let Some(chat_id) = chat_id { - if !mime_parser.has_chat_version() - && is_probably_private_reply(context, mime_parser, chat_id).await? - { - return Ok(None); - } - } - let create_protected = if mime_parser.get_header(HeaderDef::ChatVerified).is_some() { if let VerifiedEncryption::NotVerified(err) = verified_encryption { warn!(context, "Verification problem: {err:#}."); From 44f0045a1d370a87fa3d23b60a64b92ea4eeca84 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 23 May 2025 01:21:27 +0000 Subject: [PATCH 289/381] lookup_chat_by_reply refactor --- src/receive_imf.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index d2ef6aab6f..977b56bd57 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -2182,22 +2182,23 @@ async fn lookup_chat_by_reply( mime_parser: &MimeMessage, parent: &Message, ) -> Result> { - // Try to assign message to the same chat as the parent message. + debug_assert!(mime_parser.get_chat_group_id().is_none()); + // Try to assign message to the same chat as the parent message. let Some(parent_chat_id) = ChatId::lookup_by_message(parent) else { return Ok(None); }; - let parent_chat = Chat::load_from_db(context, parent_chat_id).await?; // If this was a private message just to self, it was probably a private reply. // It should not go into the group then, but into the private chat. - if is_probably_private_reply(context, mime_parser, parent_chat.id).await? { + if is_probably_private_reply(context, mime_parser, parent_chat_id).await? { return Ok(None); } // If the parent chat is a 1:1 chat, and the sender added // a new person to TO/CC, then the message should not go to the 1:1 chat, but to a // newly created ad-hoc group. + let parent_chat = Chat::load_from_db(context, parent_chat_id).await?; if parent_chat.typ == Chattype::Single && mime_parser.recipients.len() > 1 { return Ok(None); } @@ -2209,7 +2210,7 @@ async fn lookup_chat_by_reply( info!( context, - "Assigning message to {} as it's a reply to {}.", parent_chat.id, parent.rfc724_mid + "Assigning message to {parent_chat_id} as it's a reply to {}.", parent.rfc724_mid ); Ok(Some((parent_chat.id, parent_chat.blocked))) } From 62ccdb780079daa4e4ba355b14e24fb41bf617b6 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 23 May 2025 01:32:38 +0000 Subject: [PATCH 290/381] factoring out chat lookup by references --- src/receive_imf.rs | 96 ++++++++++++++++++++++++++-------------------- 1 file changed, 54 insertions(+), 42 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 977b56bd57..2ed0c587a6 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1202,21 +1202,32 @@ async fn add_parts( } } ChatAssignment::AdHocGroup | ChatAssignment::OneOneChat => { - if let Some((new_chat_id, new_chat_id_blocked)) = - lookup_chat_or_create_adhoc_group( - context, - mime_parser, - &parent_message, - to_ids, - from_id, - allow_creation || test_normal_chat.is_some(), - create_blocked, - is_partial_download.is_some(), - ) - .await? - { - chat_id = Some(new_chat_id); - chat_id_blocked = new_chat_id_blocked; + if let Some(parent) = &parent_message { + if let Some((new_chat_id, new_chat_id_blocked)) = + // Try to assign to a chat based on In-Reply-To/References. + lookup_chat_by_reply(context, mime_parser, parent).await? + { + chat_id = Some(new_chat_id); + chat_id_blocked = new_chat_id_blocked; + } + } + + if chat_id.is_none() { + if let Some((new_chat_id, new_chat_id_blocked)) = + lookup_or_create_adhoc_group( + context, + mime_parser, + to_ids, + from_id, + allow_creation || test_normal_chat.is_some(), + create_blocked, + is_partial_download.is_some(), + ) + .await? + { + chat_id = Some(new_chat_id); + chat_id_blocked = new_chat_id_blocked; + } } } } @@ -1403,21 +1414,32 @@ async fn add_parts( } } _ => { - if let Some((new_chat_id, new_chat_id_blocked)) = - lookup_chat_or_create_adhoc_group( - context, - mime_parser, - &parent_message, - to_ids, - from_id, - allow_creation, - Blocked::Not, - is_partial_download.is_some(), - ) - .await? - { - chat_id = Some(new_chat_id); - chat_id_blocked = new_chat_id_blocked; + if let Some(parent) = &parent_message { + if let Some((new_chat_id, new_chat_id_blocked)) = + // Try to assign to a chat based on In-Reply-To/References. + lookup_chat_by_reply(context, mime_parser, parent).await? + { + chat_id = Some(new_chat_id); + chat_id_blocked = new_chat_id_blocked; + } + } + + if chat_id.is_none() { + if let Some((new_chat_id, new_chat_id_blocked)) = + lookup_or_create_adhoc_group( + context, + mime_parser, + to_ids, + from_id, + allow_creation, + Blocked::Not, + is_partial_download.is_some(), + ) + .await? + { + chat_id = Some(new_chat_id); + chat_id_blocked = new_chat_id_blocked; + } } } } @@ -2216,26 +2238,15 @@ async fn lookup_chat_by_reply( } #[expect(clippy::too_many_arguments)] -async fn lookup_chat_or_create_adhoc_group( +async fn lookup_or_create_adhoc_group( context: &Context, mime_parser: &MimeMessage, - parent: &Option, to_ids: &[Option], from_id: ContactId, allow_creation: bool, create_blocked: Blocked, is_partial_download: bool, ) -> Result> { - let to_ids: Vec = to_ids.iter().filter_map(|x| *x).collect(); - - if let Some(parent) = parent { - if let Some((new_chat_id, new_chat_id_blocked)) = - // Try to assign to a chat based on In-Reply-To/References. - lookup_chat_by_reply(context, mime_parser, parent).await? - { - return Ok(Some((new_chat_id, new_chat_id_blocked))); - } - } // Partial download may be an encrypted message with protected Subject header. We do not want to // create a group with "..." or "Encrypted message" as a subject. The same is for undecipherable // messages. Instead, assign the message to 1:1 chat with the sender. @@ -2258,6 +2269,7 @@ async fn lookup_chat_or_create_adhoc_group( .get_subject() .map(|s| remove_subject_prefix(&s)) .unwrap_or_else(|| "👥📧".to_string()); + let to_ids: Vec = to_ids.iter().filter_map(|x| *x).collect(); let mut contact_ids = Vec::with_capacity(to_ids.len() + 1); contact_ids.extend(&to_ids); if !contact_ids.contains(&from_id) { From e67993271d2131fdee507892e12c2021f345fdbc Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 23 May 2025 01:40:13 +0000 Subject: [PATCH 291/381] not too many arguments --- src/receive_imf.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 2ed0c587a6..f628ad648b 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -2237,7 +2237,6 @@ async fn lookup_chat_by_reply( Ok(Some((parent_chat.id, parent_chat.blocked))) } -#[expect(clippy::too_many_arguments)] async fn lookup_or_create_adhoc_group( context: &Context, mime_parser: &MimeMessage, From da60ab65f4f77946d3511c07ec4bd113384ad867 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 23 May 2025 01:48:18 +0000 Subject: [PATCH 292/381] ChatAssignment::ExistingChat --- src/receive_imf.rs | 131 +++++++++++++++++++++++++++------------------ 1 file changed, 78 insertions(+), 53 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index f628ad648b..5eefa70b78 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -114,6 +114,11 @@ enum ChatAssignment { /// This is not encrypted. AdHocGroup, + ExistingChat { + chat_id: ChatId, + chat_id_blocked: Blocked, + }, + /// 1:1 chat with a single contact. /// /// The chat may be encrypted or not, @@ -464,6 +469,19 @@ pub(crate) async fn receive_imf_inner( ChatAssignment::GroupChat { grpid: grpid.to_string(), } + } else if let Some(parent) = &parent_message { + if let Some((chat_id, chat_id_blocked)) = + // Try to assign to a chat based on In-Reply-To/References. + lookup_chat_by_reply(context, &mime_parser, parent).await? + { + // Try to assign to a chat based on In-Reply-To/References. + ChatAssignment::ExistingChat { + chat_id, + chat_id_blocked, + } + } else { + ChatAssignment::AdHocGroup + } } else { // Could be a message from old version // with opportunistic encryption. @@ -477,6 +495,21 @@ pub(crate) async fn receive_imf_inner( } else if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() { let _listid = mailinglist_header_listid(mailinglist_header)?; ChatAssignment::MailingList + } else if let Some(parent) = &parent_message { + if let Some((chat_id, chat_id_blocked)) = + // Try to assign to a chat based on In-Reply-To/References. + lookup_chat_by_reply(context, &mime_parser, parent).await? + { + // Try to assign to a chat based on In-Reply-To/References. + ChatAssignment::ExistingChat { + chat_id, + chat_id_blocked, + } + } else if mime_parser.recipients.len() == 1 { + ChatAssignment::OneOneChat + } else { + ChatAssignment::AdHocGroup + } } else if mime_parser.recipients.len() == 1 { ChatAssignment::OneOneChat } else { @@ -507,6 +540,7 @@ pub(crate) async fn receive_imf_inner( // to lookup PGP-contacts. None } + ChatAssignment::ExistingChat { chat_id, .. } => Some(chat_id), ChatAssignment::MailingList => None, ChatAssignment::OneOneChat => { if is_partial_download.is_none() && !mime_parser.incoming { @@ -579,7 +613,10 @@ pub(crate) async fn receive_imf_inner( to_ids = Vec::new(); past_ids = Vec::new(); } - ChatAssignment::AdHocGroup => { + ChatAssignment::ExistingChat { .. } | ChatAssignment::AdHocGroup => { + // TODO separate ExistingChat and lookup PGP contacts + // if chat is encrypted. + to_ids = add_or_lookup_contacts_by_address_list( context, &mime_parser.recipients, @@ -1201,33 +1238,27 @@ async fn add_parts( } } } + ChatAssignment::ExistingChat { + chat_id: new_chat_id, + chat_id_blocked: new_chat_id_blocked, + } => { + chat_id = Some(*new_chat_id); + chat_id_blocked = *new_chat_id_blocked; + } ChatAssignment::AdHocGroup | ChatAssignment::OneOneChat => { - if let Some(parent) = &parent_message { - if let Some((new_chat_id, new_chat_id_blocked)) = - // Try to assign to a chat based on In-Reply-To/References. - lookup_chat_by_reply(context, mime_parser, parent).await? - { - chat_id = Some(new_chat_id); - chat_id_blocked = new_chat_id_blocked; - } - } - - if chat_id.is_none() { - if let Some((new_chat_id, new_chat_id_blocked)) = - lookup_or_create_adhoc_group( - context, - mime_parser, - to_ids, - from_id, - allow_creation || test_normal_chat.is_some(), - create_blocked, - is_partial_download.is_some(), - ) - .await? - { - chat_id = Some(new_chat_id); - chat_id_blocked = new_chat_id_blocked; - } + if let Some((new_chat_id, new_chat_id_blocked)) = lookup_or_create_adhoc_group( + context, + mime_parser, + to_ids, + from_id, + allow_creation || test_normal_chat.is_some(), + create_blocked, + is_partial_download.is_some(), + ) + .await? + { + chat_id = Some(new_chat_id); + chat_id_blocked = new_chat_id_blocked; } } } @@ -1413,33 +1444,27 @@ async fn add_parts( } } } + ChatAssignment::ExistingChat { + chat_id: new_chat_id, + chat_id_blocked: new_chat_id_blocked, + } => { + chat_id = Some(*new_chat_id); + chat_id_blocked = *new_chat_id_blocked; + } _ => { - if let Some(parent) = &parent_message { - if let Some((new_chat_id, new_chat_id_blocked)) = - // Try to assign to a chat based on In-Reply-To/References. - lookup_chat_by_reply(context, mime_parser, parent).await? - { - chat_id = Some(new_chat_id); - chat_id_blocked = new_chat_id_blocked; - } - } - - if chat_id.is_none() { - if let Some((new_chat_id, new_chat_id_blocked)) = - lookup_or_create_adhoc_group( - context, - mime_parser, - to_ids, - from_id, - allow_creation, - Blocked::Not, - is_partial_download.is_some(), - ) - .await? - { - chat_id = Some(new_chat_id); - chat_id_blocked = new_chat_id_blocked; - } + if let Some((new_chat_id, new_chat_id_blocked)) = lookup_or_create_adhoc_group( + context, + mime_parser, + to_ids, + from_id, + allow_creation, + Blocked::Not, + is_partial_download.is_some(), + ) + .await? + { + chat_id = Some(new_chat_id); + chat_id_blocked = new_chat_id_blocked; } } } From 9a7078e88dd1cfd273b85738eee94d53f2406d2d Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 03:04:10 +0000 Subject: [PATCH 293/381] derive debug for chat assignment --- src/receive_imf.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 5eefa70b78..4ceb735060 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -81,6 +81,7 @@ pub struct ReceivedMsg { /// don't assign the message to an encrypted /// group after looking up PGP-contacts /// or vice versa. +#[derive(Debug)] enum ChatAssignment { /// Trash the message. Trash, From 99a21855b749c0ae1b73b4c080872239ed465820 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 04:14:05 +0000 Subject: [PATCH 294/381] no non-members in 1:1 chats --- src/receive_imf.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 4ceb735060..2a3b551f74 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1282,7 +1282,7 @@ async fn add_parts( if let Some(group_chat_id) = chat_id { if !chat::is_contact_in_chat(context, group_chat_id, from_id).await? { let chat = Chat::load_from_db(context, group_chat_id).await?; - if chat.is_protected() && chat.typ == Chattype::Single { + if chat.typ == Chattype::Single { // Just assign the message to the 1:1 chat with the actual sender instead. chat_id = None; } else { From 85edc8dfd6661af651df2dff490f7370712d90d1 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 04:29:05 +0000 Subject: [PATCH 295/381] trash classic emails early --- src/receive_imf.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 2a3b551f74..f37bf738be 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -432,6 +432,16 @@ pub(crate) async fn receive_imf_inner( .await?; } true + } else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage + && !mime_parser.has_chat_version() + && parent_message.as_ref().is_none_or(|p| p.is_dc_message == MessengerMessage::No) + && !context.get_config_bool(Config::IsChatmail).await? + && ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default() == ShowEmails::Off + { + info!(context, "Classical email not shown (TRASH)."); + // the message is a classic email in a classic profile + // (in chatmail profiles, we always show all messages, because shared dc-mua usage is not supported) + true } else if mime_parser .get_header(HeaderDef::XMozillaDraftInfo) .is_some() From 792cf2355f139b37de1f1912a4cdf3612b4380e6 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 04:42:15 +0000 Subject: [PATCH 296/381] remove always-true check --- src/receive_imf.rs | 213 ++++++++++++++++++++++----------------------- 1 file changed, 102 insertions(+), 111 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index f37bf738be..0ceb8b02be 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1141,12 +1141,7 @@ async fn add_parts( // the message is a classic email in a classic profile // (in chatmail profiles, we always show all messages, because shared dc-mua usage is not supported) match show_emails { - ShowEmails::Off => { - info!(context, "Classical email not shown (TRASH)."); - chat_id = Some(DC_CHAT_ID_TRASH); - false - } - ShowEmails::AcceptedContacts => false, + ShowEmails::Off | ShowEmails::AcceptedContacts => false, ShowEmails::All => true, } } else { @@ -1200,71 +1195,28 @@ async fn add_parts( create_blocked_default }; - if chat_id.is_none() { - match &chat_assignment { - ChatAssignment::Trash => { - chat_id = Some(DC_CHAT_ID_TRASH); - } - ChatAssignment::GroupChat { grpid } => { - // Try to assign to a chat based on Chat-Group-ID. - if let Some((id, _protected, blocked)) = - chat::get_chat_id_by_grpid(context, grpid).await? - { - chat_id = Some(id); - chat_id_blocked = blocked; - } else if allow_creation || test_normal_chat.is_some() { - if let Some((new_chat_id, new_chat_id_blocked)) = create_group( - context, - mime_parser, - is_partial_download.is_some(), - create_blocked, - from_id, - to_ids, - past_ids, - &verified_encryption, - grpid, - ) - .await? - { - chat_id = Some(new_chat_id); - chat_id_blocked = new_chat_id_blocked; - } - } - } - ChatAssignment::MailingList => { - if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() { - if let Some((new_chat_id, new_chat_id_blocked)) = - create_or_lookup_mailinglist( - context, - allow_creation, - mailinglist_header, - mime_parser, - ) - .await? - { - chat_id = Some(new_chat_id); - chat_id_blocked = new_chat_id_blocked; - - apply_mailinglist_changes(context, mime_parser, new_chat_id).await?; - } - } - } - ChatAssignment::ExistingChat { - chat_id: new_chat_id, - chat_id_blocked: new_chat_id_blocked, - } => { - chat_id = Some(*new_chat_id); - chat_id_blocked = *new_chat_id_blocked; - } - ChatAssignment::AdHocGroup | ChatAssignment::OneOneChat => { - if let Some((new_chat_id, new_chat_id_blocked)) = lookup_or_create_adhoc_group( + match &chat_assignment { + ChatAssignment::Trash => { + chat_id = Some(DC_CHAT_ID_TRASH); + } + ChatAssignment::GroupChat { grpid } => { + // Try to assign to a chat based on Chat-Group-ID. + if let Some((id, _protected, blocked)) = + chat::get_chat_id_by_grpid(context, grpid).await? + { + chat_id = Some(id); + chat_id_blocked = blocked; + } else if allow_creation || test_normal_chat.is_some() { + if let Some((new_chat_id, new_chat_id_blocked)) = create_group( context, mime_parser, - to_ids, - from_id, - allow_creation || test_normal_chat.is_some(), - create_blocked, is_partial_download.is_some(), + create_blocked, + from_id, + to_ids, + past_ids, + &verified_encryption, + grpid, ) .await? { @@ -1273,6 +1225,47 @@ async fn add_parts( } } } + ChatAssignment::MailingList => { + if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() { + if let Some((new_chat_id, new_chat_id_blocked)) = + create_or_lookup_mailinglist( + context, + allow_creation, + mailinglist_header, + mime_parser, + ) + .await? + { + chat_id = Some(new_chat_id); + chat_id_blocked = new_chat_id_blocked; + + apply_mailinglist_changes(context, mime_parser, new_chat_id).await?; + } + } + } + ChatAssignment::ExistingChat { + chat_id: new_chat_id, + chat_id_blocked: new_chat_id_blocked, + } => { + chat_id = Some(*new_chat_id); + chat_id_blocked = *new_chat_id_blocked; + } + ChatAssignment::AdHocGroup | ChatAssignment::OneOneChat => { + if let Some((new_chat_id, new_chat_id_blocked)) = lookup_or_create_adhoc_group( + context, + mime_parser, + to_ids, + from_id, + allow_creation || test_normal_chat.is_some(), + create_blocked, + is_partial_download.is_some(), + ) + .await? + { + chat_id = Some(new_chat_id); + chat_id_blocked = new_chat_id_blocked; + } + } } // if the chat is somehow blocked but we want to create a non-blocked chat, @@ -1425,52 +1418,27 @@ async fn add_parts( // with only a single `hidden-recipients` group in this case. let self_sent = to_ids.len() <= 1 && to_id == ContactId::SELF; - if chat_id.is_none() { - match &chat_assignment { - ChatAssignment::Trash => { - chat_id = Some(DC_CHAT_ID_TRASH); - } - ChatAssignment::GroupChat { grpid } => { - if let Some((id, _protected, blocked)) = - chat::get_chat_id_by_grpid(context, grpid).await? - { - chat_id = Some(id); - chat_id_blocked = blocked; - } else if allow_creation { - if let Some((new_chat_id, new_chat_id_blocked)) = create_group( - context, - mime_parser, - is_partial_download.is_some(), - Blocked::Not, - from_id, - to_ids, - past_ids, - &verified_encryption, - grpid, - ) - .await? - { - chat_id = Some(new_chat_id); - chat_id_blocked = new_chat_id_blocked; - } - } - } - ChatAssignment::ExistingChat { - chat_id: new_chat_id, - chat_id_blocked: new_chat_id_blocked, - } => { - chat_id = Some(*new_chat_id); - chat_id_blocked = *new_chat_id_blocked; - } - _ => { - if let Some((new_chat_id, new_chat_id_blocked)) = lookup_or_create_adhoc_group( + match &chat_assignment { + ChatAssignment::Trash => { + chat_id = Some(DC_CHAT_ID_TRASH); + } + ChatAssignment::GroupChat { grpid } => { + if let Some((id, _protected, blocked)) = + chat::get_chat_id_by_grpid(context, grpid).await? + { + chat_id = Some(id); + chat_id_blocked = blocked; + } else if allow_creation { + if let Some((new_chat_id, new_chat_id_blocked)) = create_group( context, mime_parser, - to_ids, - from_id, - allow_creation, - Blocked::Not, is_partial_download.is_some(), + Blocked::Not, + from_id, + to_ids, + past_ids, + &verified_encryption, + grpid, ) .await? { @@ -1479,6 +1447,29 @@ async fn add_parts( } } } + ChatAssignment::ExistingChat { + chat_id: new_chat_id, + chat_id_blocked: new_chat_id_blocked, + } => { + chat_id = Some(*new_chat_id); + chat_id_blocked = *new_chat_id_blocked; + } + _ => { + if let Some((new_chat_id, new_chat_id_blocked)) = lookup_or_create_adhoc_group( + context, + mime_parser, + to_ids, + from_id, + allow_creation, + Blocked::Not, + is_partial_download.is_some(), + ) + .await? + { + chat_id = Some(new_chat_id); + chat_id_blocked = new_chat_id_blocked; + } + } } if !to_ids.is_empty() { From 98fcd09f46713b008a7ee85bc40f06e5af85a032 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 04:43:47 +0000 Subject: [PATCH 297/381] process ChatAssignment::MailingList --- src/receive_imf.rs | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 0ceb8b02be..2d0fb65f17 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1454,6 +1454,21 @@ async fn add_parts( chat_id = Some(*new_chat_id); chat_id_blocked = *new_chat_id_blocked; } + ChatAssignment::MailingList => { + // Check if the message belongs to a broadcast list. + if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() { + let listid = mailinglist_header_listid(mailinglist_header)?; + chat_id = Some( + if let Some((id, ..)) = chat::get_chat_id_by_grpid(context, &listid).await? { + id + } else { + let name = + compute_mailinglist_name(mailinglist_header, &listid, mime_parser); + chat::create_broadcast_list_ex(context, Nosync, listid, name).await? + }, + ); + } + } _ => { if let Some((new_chat_id, new_chat_id_blocked)) = lookup_or_create_adhoc_group( context, @@ -1517,22 +1532,6 @@ async fn add_parts( .await?; } - if chat_id.is_none() { - // Check if the message belongs to a broadcast list. - if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() { - let listid = mailinglist_header_listid(mailinglist_header)?; - chat_id = Some( - if let Some((id, ..)) = chat::get_chat_id_by_grpid(context, &listid).await? { - id - } else { - let name = - compute_mailinglist_name(mailinglist_header, &listid, mime_parser); - chat::create_broadcast_list_ex(context, Nosync, listid, name).await? - }, - ); - } - } - if chat_id.is_none() && self_sent { // from_id==to_id==ContactId::SELF - this is a self-sent messages, // maybe an Autocrypt Setup Message From e70b5ce5eabde080e93fd9c4935ccc634d6a36c2 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 04:55:50 +0000 Subject: [PATCH 298/381] no wildcard match on ChatAssignment --- src/receive_imf.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 2d0fb65f17..313e891863 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1469,7 +1469,7 @@ async fn add_parts( ); } } - _ => { + ChatAssignment::AdHocGroup | ChatAssignment::OneOneChat => { if let Some((new_chat_id, new_chat_id_blocked)) = lookup_or_create_adhoc_group( context, mime_parser, From e3f2946879cbce6ebcf6dcf7770789a65f328f22 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 05:22:14 +0000 Subject: [PATCH 299/381] unblock any chats for outgoing messages --- src/receive_imf.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 313e891863..12e2d78500 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1509,13 +1509,13 @@ async fn add_parts( chat_id_blocked = chat.blocked; } } + } - // automatically unblock chat when the user sends a message - if chat_id_blocked != Blocked::Not { - if let Some(chat_id) = chat_id { - chat_id.unblock_ex(context, Nosync).await?; - // Not assigning `chat_id_blocked = Blocked::Not` to avoid unused_assignments warning. - } + // automatically unblock chat when the user sends a message + if chat_id_blocked != Blocked::Not { + if let Some(chat_id) = chat_id { + chat_id.unblock_ex(context, Nosync).await?; + // Not assigning `chat_id_blocked = Blocked::Not` to avoid unused_assignments warning. } } From 66a3dfc3682bfe9762076b3431e34f9e413d53f7 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 05:26:31 +0000 Subject: [PATCH 300/381] group chat assignment --- src/receive_imf.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 12e2d78500..e5507f1da1 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1511,6 +1511,21 @@ async fn add_parts( } } + if chat_id.is_none() && self_sent { + // from_id==to_id==ContactId::SELF - this is a self-sent messages, + // maybe an Autocrypt Setup Message + let chat = ChatIdBlocked::get_for_contact(context, ContactId::SELF, Blocked::Not) + .await + .context("Failed to get (new) chat for contact")?; + + chat_id = Some(chat.id); + // Not assigning `chat_id_blocked = chat.blocked` to avoid unused_assignments warning. + + if Blocked::Not != chat.blocked { + chat.id.unblock_ex(context, Nosync).await?; + } + } + // automatically unblock chat when the user sends a message if chat_id_blocked != Blocked::Not { if let Some(chat_id) = chat_id { @@ -1531,21 +1546,6 @@ async fn add_parts( ) .await?; } - - if chat_id.is_none() && self_sent { - // from_id==to_id==ContactId::SELF - this is a self-sent messages, - // maybe an Autocrypt Setup Message - let chat = ChatIdBlocked::get_for_contact(context, ContactId::SELF, Blocked::Not) - .await - .context("Failed to get (new) chat for contact")?; - - chat_id = Some(chat.id); - // Not assigning `chat_id_blocked = chat.blocked` to avoid unused_assignments warning. - - if Blocked::Not != chat.blocked { - chat.id.unblock_ex(context, Nosync).await?; - } - } } let orig_chat_id = chat_id; From 279640b24f060e67c0805883fe224eb4797ce91d Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 05:31:39 +0000 Subject: [PATCH 301/381] refactoring --- src/receive_imf.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index e5507f1da1..2b02a2e5a6 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1282,15 +1282,16 @@ async fn add_parts( // In lookup_chat_by_reply() and create_group(), it can happen that the message is put into a chat // but the From-address is not a member of this chat. - if let Some(group_chat_id) = chat_id { - if !chat::is_contact_in_chat(context, group_chat_id, from_id).await? { - let chat = Chat::load_from_db(context, group_chat_id).await?; + if let Some(nonopt_chat_id) = chat_id { + if !chat::is_contact_in_chat(context, nonopt_chat_id, from_id).await? { + let chat = Chat::load_from_db(context, nonopt_chat_id).await?; if chat.typ == Chattype::Single { // Just assign the message to the 1:1 chat with the actual sender instead. chat_id = None; } else { - // In non-protected chats, just mark the sender as overridden. Therefore, the UI will prepend `~` - // to the sender's name, indicating to the user that he/she is not part of the group. + // Mark the sender as overridden. + // The UI will prepend `~` to the sender's name, + // indicating that the sender is not part of the group. let from = &mime_parser.from; let name: &str = from.display_name.as_ref().unwrap_or(&from.addr); for part in &mut mime_parser.parts { @@ -1304,11 +1305,13 @@ async fn add_parts( } } } + } + if let Some(chat_id) = chat_id { group_changes = apply_group_changes( context, mime_parser, - group_chat_id, + chat_id, from_id, to_ids, past_ids, From 9ba0ad012ef1275cd9460c9b865930472109ee7e Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 05:33:24 +0000 Subject: [PATCH 302/381] do not unassign messages from chats --- src/receive_imf.rs | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 2b02a2e5a6..0f8555005b 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1285,23 +1285,19 @@ async fn add_parts( if let Some(nonopt_chat_id) = chat_id { if !chat::is_contact_in_chat(context, nonopt_chat_id, from_id).await? { let chat = Chat::load_from_db(context, nonopt_chat_id).await?; - if chat.typ == Chattype::Single { - // Just assign the message to the 1:1 chat with the actual sender instead. - chat_id = None; - } else { - // Mark the sender as overridden. - // The UI will prepend `~` to the sender's name, - // indicating that the sender is not part of the group. - let from = &mime_parser.from; - let name: &str = from.display_name.as_ref().unwrap_or(&from.addr); - for part in &mut mime_parser.parts { - part.param.set(Param::OverrideSenderDisplayname, name); - - if chat.is_protected() { - // In protected chat, also mark the message with an error. - let s = stock_str::unknown_sender_for_chat(context).await; - part.error = Some(s); - } + + // Mark the sender as overridden. + // The UI will prepend `~` to the sender's name, + // indicating that the sender is not part of the group. + let from = &mime_parser.from; + let name: &str = from.display_name.as_ref().unwrap_or(&from.addr); + for part in &mut mime_parser.parts { + part.param.set(Param::OverrideSenderDisplayname, name); + + if chat.is_protected() { + // In protected chat, also mark the message with an error. + let s = stock_str::unknown_sender_for_chat(context).await; + part.error = Some(s); } } } From 205c8a2d5414d8fe28c3d20fb91289e698048f72 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 05:37:57 +0000 Subject: [PATCH 303/381] group chat assignment --- src/receive_imf.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 0f8555005b..1f95fbd59f 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1303,19 +1303,6 @@ async fn add_parts( } } - if let Some(chat_id) = chat_id { - group_changes = apply_group_changes( - context, - mime_parser, - chat_id, - from_id, - to_ids, - past_ids, - &verified_encryption, - ) - .await?; - } - if chat_id.is_none() { // try to create a normal chat let contact = Contact::get_by_id(context, from_id).await?; @@ -1396,6 +1383,19 @@ async fn add_parts( } } + if let Some(chat_id) = chat_id { + group_changes = apply_group_changes( + context, + mime_parser, + chat_id, + from_id, + to_ids, + past_ids, + &verified_encryption, + ) + .await?; + } + state = if seen || is_mdn || chat_id_blocked == Blocked::Yes || group_changes.silent // No check for `hidden` because only reactions are such and they should be `InFresh`. { From 6f60473f2f5a841acb29fcbc05b5052fc1d8e6ec Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 05:43:03 +0000 Subject: [PATCH 304/381] assign chat_id_blocked --- src/receive_imf.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 1f95fbd59f..fb6ee41575 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1518,7 +1518,7 @@ async fn add_parts( .context("Failed to get (new) chat for contact")?; chat_id = Some(chat.id); - // Not assigning `chat_id_blocked = chat.blocked` to avoid unused_assignments warning. + chat_id_blocked = chat.blocked; if Blocked::Not != chat.blocked { chat.id.unblock_ex(context, Nosync).await?; From 588b85b034ee4fd4ddefbd6d60d93f35245d9145 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 07:06:45 +0000 Subject: [PATCH 305/381] move chat assignment closer to the beginnig for add_parts --- src/receive_imf.rs | 76 +++++++++++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 35 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index fb6ee41575..9400868192 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -434,9 +434,13 @@ pub(crate) async fn receive_imf_inner( true } else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage && !mime_parser.has_chat_version() - && parent_message.as_ref().is_none_or(|p| p.is_dc_message == MessengerMessage::No) + && parent_message + .as_ref() + .is_none_or(|p| p.is_dc_message == MessengerMessage::No) && !context.get_config_bool(Config::IsChatmail).await? - && ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default() == ShowEmails::Off + && ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?) + .unwrap_or_default() + == ShowEmails::Off { info!(context, "Classical email not shown (TRASH)."); // the message is a classic email in a classic profile @@ -1101,15 +1105,8 @@ async fn add_parts( chat_assignment: ChatAssignment, ) -> Result { let is_bot = context.get_config_bool(Config::Bot).await?; - let rfc724_mid_orig = &mime_parser - .get_rfc724_mid() - .unwrap_or(rfc724_mid.to_string()); - let mut better_msg = None; let mut group_changes = GroupChangesInfo::default(); - if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled { - better_msg = Some(stock_str::msg_location_enabled_by(context, from_id).await); - } let is_dc_message = if mime_parser.has_chat_version() { MessengerMessage::Yes @@ -1149,8 +1146,6 @@ async fn add_parts( }; let to_id: ContactId; - let state: MessageState; - let hidden = is_reaction; let mut needs_delete_job = false; let mut restore_protection = false; @@ -1227,14 +1222,13 @@ async fn add_parts( } ChatAssignment::MailingList => { if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() { - if let Some((new_chat_id, new_chat_id_blocked)) = - create_or_lookup_mailinglist( - context, - allow_creation, - mailinglist_header, - mime_parser, - ) - .await? + if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_mailinglist( + context, + allow_creation, + mailinglist_header, + mime_parser, + ) + .await? { chat_id = Some(new_chat_id); chat_id_blocked = new_chat_id_blocked; @@ -1395,20 +1389,9 @@ async fn add_parts( ) .await?; } - - state = if seen || is_mdn || chat_id_blocked == Blocked::Yes || group_changes.silent - // No check for `hidden` because only reactions are such and they should be `InFresh`. - { - MessageState::InSeen - } else { - MessageState::InFresh - }; } else { // Outgoing - // the mail is on the IMAP server, probably it is also delivered. - // We cannot recreate other states (read, error). - state = MessageState::OutDelivered; to_id = to_ids.first().copied().flatten().unwrap_or(ContactId::SELF); // Older Delta Chat versions with core <=1.152.2 only accepted @@ -1458,7 +1441,8 @@ async fn add_parts( if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() { let listid = mailinglist_header_listid(mailinglist_header)?; chat_id = Some( - if let Some((id, ..)) = chat::get_chat_id_by_grpid(context, &listid).await? { + if let Some((id, ..)) = chat::get_chat_id_by_grpid(context, &listid).await? + { id } else { let name = @@ -1547,6 +1531,10 @@ async fn add_parts( } } + let rfc724_mid_orig = &mime_parser + .get_rfc724_mid() + .unwrap_or(rfc724_mid.to_string()); + let orig_chat_id = chat_id; let mut chat_id = chat_id.unwrap_or_else(|| { info!(context, "No chat id for message (TRASH)."); @@ -1568,7 +1556,19 @@ async fn add_parts( EphemeralTimer::Disabled }; + let state = if !mime_parser.incoming { + // the mail is on the IMAP server, probably it is also delivered. + // We cannot recreate other states (read, error). + MessageState::OutDelivered + } else if seen || is_mdn || chat_id_blocked == Blocked::Yes || group_changes.silent + // No check for `hidden` because only reactions are such and they should be `InFresh`. + { + MessageState::InSeen + } else { + MessageState::InFresh + }; let in_fresh = state == MessageState::InFresh; + let sort_to_bottom = false; let received = true; let sort_timestamp = chat_id @@ -1657,9 +1657,10 @@ async fn add_parts( } } - if mime_parser.is_system_message == SystemMessage::EphemeralTimerChanged { - better_msg = Some(stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await); - + let mut better_msg = if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled + { + Some(stock_str::msg_location_enabled_by(context, from_id).await) + } else if mime_parser.is_system_message == SystemMessage::EphemeralTimerChanged { // Do not delete the system message itself. // // This prevents confusion when timer is changed @@ -1667,7 +1668,11 @@ async fn add_parts( // hour, only the message about the change to 1 // week is left. ephemeral_timer = EphemeralTimer::Disabled; - } + + Some(stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await) + } else { + None + }; // if a chat is protected and the message is fully downloaded, check additional properties if !chat_id.is_special() && is_partial_download.is_none() { @@ -1795,6 +1800,7 @@ async fn add_parts( info!(context, "Message edits/deletes existing message (TRASH)."); } + let hidden = is_reaction; let mut parts = mime_parser.parts.iter().peekable(); while let Some(part) = parts.next() { if part.is_reaction { From 329c36ec20f6dbd4cbf408e63311ce55a2616ccf Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 07:15:56 +0000 Subject: [PATCH 306/381] group group_changes --- src/receive_imf.rs | 51 +++++++++++++++++----------------------------- 1 file changed, 19 insertions(+), 32 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 9400868192..f25f8120ca 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1106,8 +1106,6 @@ async fn add_parts( ) -> Result { let is_bot = context.get_config_bool(Config::Bot).await?; - let mut group_changes = GroupChangesInfo::default(); - let is_dc_message = if mime_parser.has_chat_version() { MessengerMessage::Yes } else if let Some(parent_message) = &parent_message { @@ -1118,17 +1116,14 @@ async fn add_parts( } else { MessengerMessage::No }; - // incoming non-chat messages may be discarded - let is_location_kml = mime_parser.location_kml.is_some(); - let is_mdn = !mime_parser.mdn_reports.is_empty(); - let is_reaction = mime_parser.parts.iter().any(|part| part.is_reaction); let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default(); let mut chat_id = None; let mut chat_id_blocked = Blocked::Not; + let is_reaction = mime_parser.parts.iter().any(|part| part.is_reaction); let allow_creation = if mime_parser.decrypting_failed { false } else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage @@ -1376,19 +1371,6 @@ async fn add_parts( } } } - - if let Some(chat_id) = chat_id { - group_changes = apply_group_changes( - context, - mime_parser, - chat_id, - from_id, - to_ids, - past_ids, - &verified_encryption, - ) - .await?; - } } else { // Outgoing @@ -1516,21 +1498,26 @@ async fn add_parts( // Not assigning `chat_id_blocked = Blocked::Not` to avoid unused_assignments warning. } } - - if let Some(chat_id) = chat_id { - group_changes = apply_group_changes( - context, - mime_parser, - chat_id, - from_id, - to_ids, - past_ids, - &verified_encryption, - ) - .await?; - } } + let is_location_kml = mime_parser.location_kml.is_some(); + let is_mdn = !mime_parser.mdn_reports.is_empty(); + + let mut group_changes = if let Some(chat_id) = chat_id { + apply_group_changes( + context, + mime_parser, + chat_id, + from_id, + to_ids, + past_ids, + &verified_encryption, + ) + .await? + } else { + GroupChangesInfo::default() + }; + let rfc724_mid_orig = &mime_parser .get_rfc724_mid() .unwrap_or(rfc724_mid.to_string()); From 950f9a3153e46391d5dc1578469585ad69ad8a77 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 07:35:01 +0000 Subject: [PATCH 307/381] group chat assignment more --- src/receive_imf.rs | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index f25f8120ca..db7065966f 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1269,29 +1269,6 @@ async fn add_parts( } } - // In lookup_chat_by_reply() and create_group(), it can happen that the message is put into a chat - // but the From-address is not a member of this chat. - if let Some(nonopt_chat_id) = chat_id { - if !chat::is_contact_in_chat(context, nonopt_chat_id, from_id).await? { - let chat = Chat::load_from_db(context, nonopt_chat_id).await?; - - // Mark the sender as overridden. - // The UI will prepend `~` to the sender's name, - // indicating that the sender is not part of the group. - let from = &mime_parser.from; - let name: &str = from.display_name.as_ref().unwrap_or(&from.addr); - for part in &mut mime_parser.parts { - part.param.set(Param::OverrideSenderDisplayname, name); - - if chat.is_protected() { - // In protected chat, also mark the message with an error. - let s = stock_str::unknown_sender_for_chat(context).await; - part.error = Some(s); - } - } - } - } - if chat_id.is_none() { // try to create a normal chat let contact = Contact::get_by_id(context, from_id).await?; @@ -1371,6 +1348,29 @@ async fn add_parts( } } } + + // In lookup_chat_by_reply() and create_group(), it can happen that the message is put into a chat + // but the From-address is not a member of this chat. + if let Some(nonopt_chat_id) = chat_id { + if !chat::is_contact_in_chat(context, nonopt_chat_id, from_id).await? { + let chat = Chat::load_from_db(context, nonopt_chat_id).await?; + + // Mark the sender as overridden. + // The UI will prepend `~` to the sender's name, + // indicating that the sender is not part of the group. + let from = &mime_parser.from; + let name: &str = from.display_name.as_ref().unwrap_or(&from.addr); + for part in &mut mime_parser.parts { + part.param.set(Param::OverrideSenderDisplayname, name); + + if chat.is_protected() { + // In protected chat, also mark the message with an error. + let s = stock_str::unknown_sender_for_chat(context).await; + part.error = Some(s); + } + } + } + } } else { // Outgoing From d49e8ba670a2263884bf8d377c751baf95360186 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 07:41:20 +0000 Subject: [PATCH 308/381] direct check for has_chat_version --- src/receive_imf.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index db7065966f..fa587d41a1 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1468,7 +1468,7 @@ async fn add_parts( chat_id_blocked = chat.blocked; } } - if chat_id.is_none() && is_dc_message == MessengerMessage::Yes { + if chat_id.is_none() && mime_parser.has_chat_version() { if let Some(chat) = ChatIdBlocked::lookup_by_contact(context, to_id).await? { chat_id = Some(chat.id); chat_id_blocked = chat.blocked; From 6fb05962409faa796d8c6cf6baa23ad94f608467 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 07:41:20 +0000 Subject: [PATCH 309/381] almost immutable chat_id --- src/receive_imf.rs | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index fa587d41a1..7c5302ff68 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -408,6 +408,12 @@ pub(crate) async fn receive_imf_inner( info!(context, "Message is a DSN (TRASH)."); markseen_on_imap_table(context, rfc724_mid).await.ok(); true + } else if mime_parser.get_header(HeaderDef::ChatEdit).is_some() { + true + } else if mime_parser.get_header(HeaderDef::ChatDelete).is_some() { + true + } else if mime_parser.get_header(HeaderDef::IrohNodeAddr).is_some() { + true } else if mime_parser.sync_items.is_some() { true } else if mime_parser.decrypting_failed && !mime_parser.incoming { @@ -1140,7 +1146,13 @@ async fn add_parts( !is_reaction }; - let to_id: ContactId; + + let to_id = if mime_parser.incoming { + ContactId::SELF + } else { + to_ids.first().copied().flatten().unwrap_or(ContactId::SELF) + }; + let mut needs_delete_job = false; let mut restore_protection = false; @@ -1155,8 +1167,6 @@ async fn add_parts( } if mime_parser.incoming { - to_id = ContactId::SELF; - let test_normal_chat = ChatIdBlocked::lookup_by_contact(context, from_id).await?; let create_blocked_default = if is_bot { @@ -1374,8 +1384,6 @@ async fn add_parts( } else { // Outgoing - to_id = to_ids.first().copied().flatten().unwrap_or(ContactId::SELF); - // Older Delta Chat versions with core <=1.152.2 only accepted // self-sent messages in Saved Messages with own address in the `To` field. // New Delta Chat versions may use empty `To` field @@ -1522,7 +1530,6 @@ async fn add_parts( .get_rfc724_mid() .unwrap_or(rfc724_mid.to_string()); - let orig_chat_id = chat_id; let mut chat_id = chat_id.unwrap_or_else(|| { info!(context, "No chat id for message (TRASH)."); DC_CHAT_ID_TRASH @@ -1756,7 +1763,6 @@ async fn add_parts( } if let Some(node_addr) = mime_parser.get_header(HeaderDef::IrohNodeAddr) { - chat_id = DC_CHAT_ID_TRASH; match mime_parser.get_header(HeaderDef::InReplyTo) { Some(in_reply_to) => match rfc724_mid_exists(context, in_reply_to).await? { Some((instance_id, _ts_sent)) => { @@ -1782,10 +1788,7 @@ async fn add_parts( } } - if handle_edit_delete(context, mime_parser, from_id).await? { - chat_id = DC_CHAT_ID_TRASH; - info!(context, "Message edits/deletes existing message (TRASH)."); - } + handle_edit_delete(context, mime_parser, from_id).await?; let hidden = is_reaction; let mut parts = mime_parser.parts.iter().peekable(); @@ -1796,7 +1799,7 @@ async fn add_parts( set_msg_reaction( context, mime_in_reply_to, - orig_chat_id.unwrap_or_default(), + chat_id, from_id, sort_timestamp, Reaction::from(reaction_str.as_str()), @@ -2056,7 +2059,7 @@ async fn handle_edit_delete( context: &Context, mime_parser: &MimeMessage, from_id: ContactId, -) -> Result { +) -> Result<()> { if let Some(rfc724_mid) = mime_parser.get_header(HeaderDef::ChatEdit) { if let Some((original_msg_id, _)) = rfc724_mid_exists(context, rfc724_mid).await? { if let Some(mut original_msg) = @@ -2089,8 +2092,6 @@ async fn handle_edit_delete( "Edit message: rfc724_mid {rfc724_mid:?} not found." ); } - - Ok(true) } else if let Some(rfc724_mid_list) = mime_parser.get_header(HeaderDef::ChatDelete) { if let Some(part) = mime_parser.parts.first() { // See `message::delete_msgs_ex()`, unlike edit requests, DC doesn't send unencrypted @@ -2124,11 +2125,8 @@ async fn handle_edit_delete( warn!(context, "Delete message: Not encrypted."); } } - - Ok(true) - } else { - Ok(false) } + Ok(()) } async fn tweak_sort_timestamp( From 081204ca226440c57a5ee179d25e4945ec29163f Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 08:17:22 +0000 Subject: [PATCH 310/381] immutable chat_id --- src/receive_imf.rs | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 7c5302ff68..a9d66cd343 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1507,12 +1507,15 @@ async fn add_parts( } } } + let chat_id = chat_id.unwrap_or_else(|| { + info!(context, "No chat id for message (TRASH)."); + DC_CHAT_ID_TRASH + }); let is_location_kml = mime_parser.location_kml.is_some(); let is_mdn = !mime_parser.mdn_reports.is_empty(); - let mut group_changes = if let Some(chat_id) = chat_id { - apply_group_changes( + let mut group_changes = apply_group_changes( context, mime_parser, chat_id, @@ -1521,20 +1524,12 @@ async fn add_parts( past_ids, &verified_encryption, ) - .await? - } else { - GroupChangesInfo::default() - }; + .await?; let rfc724_mid_orig = &mime_parser .get_rfc724_mid() .unwrap_or(rfc724_mid.to_string()); - let mut chat_id = chat_id.unwrap_or_else(|| { - info!(context, "No chat id for message (TRASH)."); - DC_CHAT_ID_TRASH - }); - // Extract ephemeral timer from the message or use the existing timer if the message is not fully downloaded. let mut ephemeral_timer = if is_partial_download.is_some() { chat_id.get_ephemeral_timer(context).await? @@ -1746,6 +1741,13 @@ async fn add_parts( } } } + + let chat_id = if better_msg.as_ref().is_some_and(|better_msg| better_msg.is_empty()) + && is_partial_download.is_none() { + DC_CHAT_ID_TRASH + } else { + chat_id + }; for (group_changes_msg, cmd, added_removed_id) in group_changes.extra_msgs { chat::add_info_msg_with_cmd( @@ -1828,9 +1830,6 @@ async fn add_parts( } let (msg, typ): (&str, Viewtype) = if let Some(better_msg) = &better_msg { - if better_msg.is_empty() && is_partial_download.is_none() { - chat_id = DC_CHAT_ID_TRASH; - } (better_msg, Viewtype::Text) } else { (&part.msg, part.typ) From 9bf73ade01020001cbd551ae5bd5ac0d77ae4b60 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 08:19:48 +0000 Subject: [PATCH 311/381] immutable needs_delete_job --- src/receive_imf.rs | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index a9d66cd343..4c2e1cc34e 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1146,14 +1146,12 @@ async fn add_parts( !is_reaction }; - let to_id = if mime_parser.incoming { ContactId::SELF } else { to_ids.first().copied().flatten().unwrap_or(ContactId::SELF) }; - let mut needs_delete_job = false; let mut restore_protection = false; // if contact renaming is prevented (for mailinglists and bots), @@ -1516,15 +1514,15 @@ async fn add_parts( let is_mdn = !mime_parser.mdn_reports.is_empty(); let mut group_changes = apply_group_changes( - context, - mime_parser, - chat_id, - from_id, - to_ids, - past_ids, - &verified_encryption, - ) - .await?; + context, + mime_parser, + chat_id, + from_id, + to_ids, + past_ids, + &verified_encryption, + ) + .await?; let rfc724_mid_orig = &mime_parser .get_rfc724_mid() @@ -1741,9 +1739,12 @@ async fn add_parts( } } } - - let chat_id = if better_msg.as_ref().is_some_and(|better_msg| better_msg.is_empty()) - && is_partial_download.is_none() { + + let chat_id = if better_msg + .as_ref() + .is_some_and(|better_msg| better_msg.is_empty()) + && is_partial_download.is_none() + { DC_CHAT_ID_TRASH } else { chat_id @@ -2024,12 +2025,12 @@ RETURNING id } } - if !mime_parser.incoming && is_mdn && is_dc_message == MessengerMessage::Yes { - // Normally outgoing MDNs sent by us never appear in mailboxes, but Gmail saves all - // outgoing messages, including MDNs, to the Sent folder. If we detect such saved MDN, - // delete it. - needs_delete_job = true; - } + // Normally outgoing MDNs sent by us never appear in mailboxes, but Gmail saves all + // outgoing messages, including MDNs, to the Sent folder. If we detect such saved MDN, + // delete it. + let needs_delete_job = + !mime_parser.incoming && is_mdn && is_dc_message == MessengerMessage::Yes; + if restore_protection { chat_id .set_protection( From d5d1d398b39a0e7718dc70c2d938911e3179bde0 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 08:24:45 +0000 Subject: [PATCH 312/381] more stuff after chat assignment --- src/receive_imf.rs | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 4c2e1cc34e..5e8183c950 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1356,29 +1356,6 @@ async fn add_parts( } } } - - // In lookup_chat_by_reply() and create_group(), it can happen that the message is put into a chat - // but the From-address is not a member of this chat. - if let Some(nonopt_chat_id) = chat_id { - if !chat::is_contact_in_chat(context, nonopt_chat_id, from_id).await? { - let chat = Chat::load_from_db(context, nonopt_chat_id).await?; - - // Mark the sender as overridden. - // The UI will prepend `~` to the sender's name, - // indicating that the sender is not part of the group. - let from = &mime_parser.from; - let name: &str = from.display_name.as_ref().unwrap_or(&from.addr); - for part in &mut mime_parser.parts { - part.param.set(Param::OverrideSenderDisplayname, name); - - if chat.is_protected() { - // In protected chat, also mark the message with an error. - let s = stock_str::unknown_sender_for_chat(context).await; - part.error = Some(s); - } - } - } - } } else { // Outgoing @@ -1510,6 +1487,29 @@ async fn add_parts( DC_CHAT_ID_TRASH }); + if mime_parser.incoming && !chat_id.is_trash() { + // It can happen that the message is put into a chat + // but the From-address is not a member of this chat. + if !chat::is_contact_in_chat(context, chat_id, from_id).await? { + let chat = Chat::load_from_db(context, chat_id).await?; + + // Mark the sender as overridden. + // The UI will prepend `~` to the sender's name, + // indicating that the sender is not part of the group. + let from = &mime_parser.from; + let name: &str = from.display_name.as_ref().unwrap_or(&from.addr); + for part in &mut mime_parser.parts { + part.param.set(Param::OverrideSenderDisplayname, name); + + if chat.is_protected() { + // In protected chat, also mark the message with an error. + let s = stock_str::unknown_sender_for_chat(context).await; + part.error = Some(s); + } + } + } + } + let is_location_kml = mime_parser.location_kml.is_some(); let is_mdn = !mime_parser.mdn_reports.is_empty(); From d27e8508ee2adfd35fc2a24f0e9ef429b45bdb78 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 08:32:54 +0000 Subject: [PATCH 313/381] prevent_rename after chat assignment --- src/receive_imf.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 5e8183c950..8b5dacae1f 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1154,16 +1154,6 @@ async fn add_parts( let mut restore_protection = false; - // if contact renaming is prevented (for mailinglists and bots), - // we use name from From:-header as override name - if prevent_rename { - if let Some(name) = &mime_parser.from.display_name { - for part in &mut mime_parser.parts { - part.param.set(Param::OverrideSenderDisplayname, name); - } - } - } - if mime_parser.incoming { let test_normal_chat = ChatIdBlocked::lookup_by_contact(context, from_id).await?; @@ -1278,7 +1268,7 @@ async fn add_parts( } if chat_id.is_none() { - // try to create a normal chat + // Try to create a 1:1 chat. let contact = Contact::get_by_id(context, from_id).await?; let create_blocked = match contact.is_blocked() { true => Blocked::Yes, @@ -1487,6 +1477,16 @@ async fn add_parts( DC_CHAT_ID_TRASH }); + // if contact renaming is prevented (for mailinglists and bots), + // we use name from From:-header as override name + if prevent_rename { + if let Some(name) = &mime_parser.from.display_name { + for part in &mut mime_parser.parts { + part.param.set(Param::OverrideSenderDisplayname, name); + } + } + } + if mime_parser.incoming && !chat_id.is_trash() { // It can happen that the message is put into a chat // but the From-address is not a member of this chat. From e717183132e8a4927a66b3bb6df3141a126ba5b6 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 08:34:25 +0000 Subject: [PATCH 314/381] remove restore_protection --- src/receive_imf.rs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 8b5dacae1f..e2ed778f68 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1152,8 +1152,6 @@ async fn add_parts( to_ids.first().copied().flatten().unwrap_or(ContactId::SELF) }; - let mut restore_protection = false; - if mime_parser.incoming { let test_normal_chat = ChatIdBlocked::lookup_by_contact(context, from_id).await?; @@ -1341,8 +1339,6 @@ async fn add_parts( ) .await?; } - restore_protection = new_protection != ProtectionStatus::Protected - && contact.is_verified(context).await?; } } } @@ -2031,16 +2027,6 @@ RETURNING id let needs_delete_job = !mime_parser.incoming && is_mdn && is_dc_message == MessengerMessage::Yes; - if restore_protection { - chat_id - .set_protection( - context, - ProtectionStatus::Protected, - mime_parser.timestamp_rcvd, - Some(from_id), - ) - .await?; - } Ok(ReceivedMsg { chat_id, state, From fba5ca333b70cbfa131d42bc63b99ef61ac0f0ce Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 09:03:06 +0000 Subject: [PATCH 315/381] factor out do_chat_assignment --- src/receive_imf.rs | 137 ++++++++++++++++++++++++++++----------------- 1 file changed, 85 insertions(+), 52 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index e2ed778f68..590481f240 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1090,68 +1090,27 @@ pub async fn from_field_to_contact_id( } } -/// Creates a `ReceivedMsg` from given parts which might consist of -/// multiple messages (if there are multiple attachments). -/// Every entry in `mime_parser.parts` produces a new row in the `msgs` table. -#[expect(clippy::too_many_arguments)] -async fn add_parts( +/// Assigns the message to a chat. +/// +/// Creates a new the chat if necessary. +async fn do_chat_assignment( context: &Context, - mime_parser: &mut MimeMessage, - imf_raw: &[u8], + chat_assignment: ChatAssignment, + from_id: ContactId, to_ids: &[Option], past_ids: &[Option], - rfc724_mid: &str, - from_id: ContactId, - seen: bool, + to_id: ContactId, + allow_creation: bool, + mime_parser: &mut MimeMessage, is_partial_download: Option, - mut replace_msg_id: Option, - prevent_rename: bool, - verified_encryption: VerifiedEncryption, + verified_encryption: &VerifiedEncryption, parent_message: Option, - chat_assignment: ChatAssignment, -) -> Result { +) -> Result<(ChatId, Blocked)> { let is_bot = context.get_config_bool(Config::Bot).await?; - let is_dc_message = if mime_parser.has_chat_version() { - MessengerMessage::Yes - } else if let Some(parent_message) = &parent_message { - match parent_message.is_dc_message { - MessengerMessage::No => MessengerMessage::No, - MessengerMessage::Yes | MessengerMessage::Reply => MessengerMessage::Reply, - } - } else { - MessengerMessage::No - }; - - let show_emails = - ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default(); - let mut chat_id = None; let mut chat_id_blocked = Blocked::Not; - let is_reaction = mime_parser.parts.iter().any(|part| part.is_reaction); - let allow_creation = if mime_parser.decrypting_failed { - false - } else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage - && is_dc_message == MessengerMessage::No - && !context.get_config_bool(Config::IsChatmail).await? - { - // the message is a classic email in a classic profile - // (in chatmail profiles, we always show all messages, because shared dc-mua usage is not supported) - match show_emails { - ShowEmails::Off | ShowEmails::AcceptedContacts => false, - ShowEmails::All => true, - } - } else { - !is_reaction - }; - - let to_id = if mime_parser.incoming { - ContactId::SELF - } else { - to_ids.first().copied().flatten().unwrap_or(ContactId::SELF) - }; - if mime_parser.incoming { let test_normal_chat = ChatIdBlocked::lookup_by_contact(context, from_id).await?; @@ -1472,6 +1431,80 @@ async fn add_parts( info!(context, "No chat id for message (TRASH)."); DC_CHAT_ID_TRASH }); + Ok((chat_id, chat_id_blocked)) +} + +/// Creates a `ReceivedMsg` from given parts which might consist of +/// multiple messages (if there are multiple attachments). +/// Every entry in `mime_parser.parts` produces a new row in the `msgs` table. +#[expect(clippy::too_many_arguments)] +async fn add_parts( + context: &Context, + mime_parser: &mut MimeMessage, + imf_raw: &[u8], + to_ids: &[Option], + past_ids: &[Option], + rfc724_mid: &str, + from_id: ContactId, + seen: bool, + is_partial_download: Option, + mut replace_msg_id: Option, + prevent_rename: bool, + verified_encryption: VerifiedEncryption, + parent_message: Option, + chat_assignment: ChatAssignment, +) -> Result { + let is_dc_message = if mime_parser.has_chat_version() { + MessengerMessage::Yes + } else if let Some(parent_message) = &parent_message { + match parent_message.is_dc_message { + MessengerMessage::No => MessengerMessage::No, + MessengerMessage::Yes | MessengerMessage::Reply => MessengerMessage::Reply, + } + } else { + MessengerMessage::No + }; + + let show_emails = + ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default(); + + let is_reaction = mime_parser.parts.iter().any(|part| part.is_reaction); + let allow_creation = if mime_parser.decrypting_failed { + false + } else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage + && is_dc_message == MessengerMessage::No + && !context.get_config_bool(Config::IsChatmail).await? + { + // the message is a classic email in a classic profile + // (in chatmail profiles, we always show all messages, because shared dc-mua usage is not supported) + match show_emails { + ShowEmails::Off | ShowEmails::AcceptedContacts => false, + ShowEmails::All => true, + } + } else { + !is_reaction + }; + + let to_id = if mime_parser.incoming { + ContactId::SELF + } else { + to_ids.first().copied().flatten().unwrap_or(ContactId::SELF) + }; + + let (chat_id, chat_id_blocked) = do_chat_assignment( + context, + chat_assignment, + from_id, + to_ids, + past_ids, + to_id, + allow_creation, + mime_parser, + is_partial_download, + &verified_encryption, + parent_message, + ) + .await?; // if contact renaming is prevented (for mailinglists and bots), // we use name from From:-header as override name From 552d86d188b0104a3a652ad1045a192d6ff456a1 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 09:04:42 +0000 Subject: [PATCH 316/381] clippy --- src/receive_imf.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 590481f240..51d0abaf8e 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -408,13 +408,11 @@ pub(crate) async fn receive_imf_inner( info!(context, "Message is a DSN (TRASH)."); markseen_on_imap_table(context, rfc724_mid).await.ok(); true - } else if mime_parser.get_header(HeaderDef::ChatEdit).is_some() { - true - } else if mime_parser.get_header(HeaderDef::ChatDelete).is_some() { - true - } else if mime_parser.get_header(HeaderDef::IrohNodeAddr).is_some() { - true - } else if mime_parser.sync_items.is_some() { + } else if mime_parser.get_header(HeaderDef::ChatEdit).is_some() + || mime_parser.get_header(HeaderDef::ChatDelete).is_some() + || mime_parser.get_header(HeaderDef::IrohNodeAddr).is_some() + || mime_parser.sync_items.is_some() + { true } else if mime_parser.decrypting_failed && !mime_parser.incoming { // Outgoing undecryptable message. From e6a79fb79f9d8c2def88990cacd3a8032cf0ef37 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 09:10:12 +0000 Subject: [PATCH 317/381] call do_chat_assignment from receive_imf --- src/receive_imf.rs | 109 ++++++++++++++++++++++++--------------------- 1 file changed, 59 insertions(+), 50 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 51d0abaf8e..e2a77a14a6 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -763,6 +763,58 @@ pub(crate) async fn receive_imf_inner( let received_msg = if let Some(received_msg) = received_msg { received_msg } else { + let is_dc_message = if mime_parser.has_chat_version() { + MessengerMessage::Yes + } else if let Some(parent_message) = &parent_message { + match parent_message.is_dc_message { + MessengerMessage::No => MessengerMessage::No, + MessengerMessage::Yes | MessengerMessage::Reply => MessengerMessage::Reply, + } + } else { + MessengerMessage::No + }; + + let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?) + .unwrap_or_default(); + + let is_reaction = mime_parser.parts.iter().any(|part| part.is_reaction); + let allow_creation = if mime_parser.decrypting_failed { + false + } else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage + && is_dc_message == MessengerMessage::No + && !context.get_config_bool(Config::IsChatmail).await? + { + // the message is a classic email in a classic profile + // (in chatmail profiles, we always show all messages, because shared dc-mua usage is not supported) + match show_emails { + ShowEmails::Off | ShowEmails::AcceptedContacts => false, + ShowEmails::All => true, + } + } else { + !is_reaction + }; + + let to_id = if mime_parser.incoming { + ContactId::SELF + } else { + to_ids.first().copied().flatten().unwrap_or(ContactId::SELF) + }; + + let (chat_id, chat_id_blocked) = do_chat_assignment( + context, + chat_assignment, + from_id, + &to_ids, + &past_ids, + to_id, + allow_creation, + &mut mime_parser, + is_partial_download, + &verified_encryption, + parent_message, + ) + .await?; + // Add parts add_parts( context, @@ -777,8 +829,9 @@ pub(crate) async fn receive_imf_inner( replace_msg_id, prevent_rename, verified_encryption, - parent_message, - chat_assignment, + chat_id, + chat_id_blocked, + is_dc_message, ) .await .context("add_parts error")? @@ -1449,61 +1502,16 @@ async fn add_parts( mut replace_msg_id: Option, prevent_rename: bool, verified_encryption: VerifiedEncryption, - parent_message: Option, - chat_assignment: ChatAssignment, + chat_id: ChatId, + chat_id_blocked: Blocked, + is_dc_message: MessengerMessage, ) -> Result { - let is_dc_message = if mime_parser.has_chat_version() { - MessengerMessage::Yes - } else if let Some(parent_message) = &parent_message { - match parent_message.is_dc_message { - MessengerMessage::No => MessengerMessage::No, - MessengerMessage::Yes | MessengerMessage::Reply => MessengerMessage::Reply, - } - } else { - MessengerMessage::No - }; - - let show_emails = - ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default(); - - let is_reaction = mime_parser.parts.iter().any(|part| part.is_reaction); - let allow_creation = if mime_parser.decrypting_failed { - false - } else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage - && is_dc_message == MessengerMessage::No - && !context.get_config_bool(Config::IsChatmail).await? - { - // the message is a classic email in a classic profile - // (in chatmail profiles, we always show all messages, because shared dc-mua usage is not supported) - match show_emails { - ShowEmails::Off | ShowEmails::AcceptedContacts => false, - ShowEmails::All => true, - } - } else { - !is_reaction - }; - let to_id = if mime_parser.incoming { ContactId::SELF } else { to_ids.first().copied().flatten().unwrap_or(ContactId::SELF) }; - let (chat_id, chat_id_blocked) = do_chat_assignment( - context, - chat_assignment, - from_id, - to_ids, - past_ids, - to_id, - allow_creation, - mime_parser, - is_partial_download, - &verified_encryption, - parent_message, - ) - .await?; - // if contact renaming is prevented (for mailinglists and bots), // we use name from From:-header as override name if prevent_rename { @@ -1820,6 +1828,7 @@ async fn add_parts( handle_edit_delete(context, mime_parser, from_id).await?; + let is_reaction = mime_parser.parts.iter().any(|part| part.is_reaction); let hidden = is_reaction; let mut parts = mime_parser.parts.iter().peekable(); while let Some(part) = parts.next() { From b31b9356e82fa86abaf2e0fb7cdd2f419551f4f3 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 09:35:25 +0000 Subject: [PATCH 318/381] clippy --- src/receive_imf.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index e2a77a14a6..4e95f125d4 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1144,6 +1144,7 @@ pub async fn from_field_to_contact_id( /// Assigns the message to a chat. /// /// Creates a new the chat if necessary. +#[expect(clippy::too_many_arguments)] async fn do_chat_assignment( context: &Context, chat_assignment: ChatAssignment, @@ -1211,7 +1212,7 @@ async fn do_chat_assignment( from_id, to_ids, past_ids, - &verified_encryption, + verified_encryption, grpid, ) .await? @@ -1380,7 +1381,7 @@ async fn do_chat_assignment( from_id, to_ids, past_ids, - &verified_encryption, + verified_encryption, grpid, ) .await? From 3d1d1e4a3b1a90cea233a59a8bd21d0ebe5c4dc4 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 16:41:10 +0000 Subject: [PATCH 319/381] log chat assignment --- src/receive_imf.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 4e95f125d4..0fbefc92ed 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -534,6 +534,7 @@ pub(crate) async fn receive_imf_inner( } else { ChatAssignment::AdHocGroup }; + info!(context, "Chat assignment is {chat_assignment:?}."); // ID of the chat to look up the addresses in. // From 5d456f03835d7643cc9c65a9e466e81ab63d73bd Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 16:41:10 +0000 Subject: [PATCH 320/381] lookup pgp contacts for existing PGP-chat --- src/receive_imf.rs | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 0fbefc92ed..41d0473f44 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -633,10 +633,40 @@ pub(crate) async fn receive_imf_inner( to_ids = Vec::new(); past_ids = Vec::new(); } - ChatAssignment::ExistingChat { .. } | ChatAssignment::AdHocGroup => { - // TODO separate ExistingChat and lookup PGP contacts - // if chat is encrypted. + ChatAssignment::ExistingChat { chat_id, .. } => { + let chat = Chat::load_from_db(context, chat_id).await?; + if chat.is_encrypted(context).await? { + to_ids = pgp_to_ids; + past_ids = lookup_pgp_contacts_by_address_list( + context, + &mime_parser.past_members, + past_member_fingerprints, + Some(chat_id), + ) + .await?; + } else { + to_ids = add_or_lookup_contacts_by_address_list( + context, + &mime_parser.recipients, + if !mime_parser.incoming { + Origin::OutgoingTo + } else if incoming_origin.is_known() { + Origin::IncomingTo + } else { + Origin::IncomingUnknownTo + }, + ) + .await?; + past_ids = add_or_lookup_contacts_by_address_list( + context, + &mime_parser.past_members, + Origin::Hidden, + ) + .await?; + } + } + ChatAssignment::AdHocGroup => { to_ids = add_or_lookup_contacts_by_address_list( context, &mime_parser.recipients, From 65bd25b4100b7f2cb6b604dd08309ea428f02483 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 17:18:10 +0000 Subject: [PATCH 321/381] test fixup --- src/receive_imf/receive_imf_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index f1f3760980..246603b5c2 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -5142,7 +5142,7 @@ async fn test_no_email_contact_added_into_group() -> Result<()> { ); // Unencrypted message should not even be assigned to encrypted chat. - assert_eq!(msg.chat_id, alice_chat_id); + assert_ne!(msg.chat_id, alice_chat_id); Ok(()) } From 6525a248af5782783be428cbb26747b704e3912c Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 18:22:39 +0000 Subject: [PATCH 322/381] remove test_protected_group_reply_from_mua --- src/receive_imf/receive_imf_tests.rs | 31 ---------------------------- 1 file changed, 31 deletions(-) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 246603b5c2..0907f5ee72 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -4502,37 +4502,6 @@ async fn test_protected_group_add_remove_member_missing_key() -> Result<()> { Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_protected_group_reply_from_mua() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = &tcm.alice().await; - let bob = &tcm.bob().await; - let fiona = &tcm.fiona().await; - mark_as_verified(alice, bob).await; - mark_as_verified(alice, fiona).await; - mark_as_verified(bob, alice).await; - let alice_chat_id = alice - .create_group_with_members(ProtectionStatus::Protected, "Group", &[bob, fiona]) - .await; - let sent = alice.send_text(alice_chat_id, "Hello!").await; - let bob_msg = bob.recv_msg(&sent).await; - bob_msg.chat_id.accept(bob).await?; - // This is hacky, but i don't know other simple way to simulate a MUA reply. It works because - // the message is correctly assigned to the chat by `References:`. - bob.sql - .execute( - "UPDATE chats SET protected=0, grpid='' WHERE id=?", - (bob_msg.chat_id,), - ) - .await?; - let sent = bob - .send_text(bob_msg.chat_id, "/me replying from MUA") - .await; - let alice_msg = alice.recv_msg(&sent).await; - assert_eq!(alice_msg.chat_id, alice_chat_id); - Ok(()) -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_older_message_from_2nd_device() -> Result<()> { let mut tcm = TestContextManager::new(); From e0b84b7e8e05152bbf68b6a249f101da91de2677 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 May 2025 21:42:05 +0000 Subject: [PATCH 323/381] encrypt test_subject_in_group --- src/mimefactory/mimefactory_tests.rs | 38 +++++++++++----------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/src/mimefactory/mimefactory_tests.rs b/src/mimefactory/mimefactory_tests.rs index ec1bfac825..97c1ee7882 100644 --- a/src/mimefactory/mimefactory_tests.rs +++ b/src/mimefactory/mimefactory_tests.rs @@ -339,39 +339,31 @@ async fn test_subject_in_group() -> Result<()> { } // 6. Test that in a group, replies also take the quoted message's subject, while non-replies use the group title as subject - let t = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let group_id = chat::create_group_chat(&t, chat::ProtectionStatus::Unprotected, "groupname") // TODO encodings, ä + let mut tcm = TestContextManager::new(); + let t = tcm.alice().await; + let bob = tcm.bob().await; + let group_id = chat::create_group_chat(&t, chat::ProtectionStatus::Unprotected, "groupname") .await .unwrap(); let bob_contact_id = t.add_or_lookup_contact_id(&bob).await; chat::add_contact_to_chat(&t, group_id, bob_contact_id).await?; + let sent_message = t.send_text(group_id, "Hello!").await; + let bob_received_message = bob.recv_msg(&sent_message).await; + let bob_group_id = bob_received_message.chat_id; + bob_group_id.accept(&bob).await.unwrap(); + assert_eq!(get_subject(&t, sent_message).await?, "groupname"); + let subject = send_msg_get_subject(&t, group_id, None).await?; - assert_eq!(subject, "groupname"); + assert_eq!(subject, "Re: groupname"); let subject = send_msg_get_subject(&t, group_id, None).await?; assert_eq!(subject, "Re: groupname"); - receive_imf( - &t, - format!( - "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: bob@example.com\n\ - To: alice@example.org\n\ - Subject: Different subject\n\ - In-Reply-To: {}\n\ - Message-ID: <2893@example.com>\n\ - Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ - \n\ - hello\n", - t.get_last_msg().await.rfc724_mid - ) - .as_bytes(), - false, - ) - .await?; - let message_from_bob = t.get_last_msg().await; + let mut msg = Message::new(Viewtype::Text); + msg.set_subject("Different subject".to_string()); + let bob_sent_msg = bob.send_msg(bob_group_id, &mut msg).await; + let message_from_bob = t.recv_msg(&bob_sent_msg).await; let subject = send_msg_get_subject(&t, group_id, None).await?; assert_eq!(subject, "Re: groupname"); From b3adddce34f458a95e4e02cce573cbc7dc43ab2b Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 25 May 2025 10:01:34 +0000 Subject: [PATCH 324/381] Allow to assign partial downloads to encrypted chats --- src/receive_imf.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 41d0473f44..b61c1fa182 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -491,7 +491,8 @@ pub(crate) async fn receive_imf_inner( } else if let Some(parent) = &parent_message { if let Some((chat_id, chat_id_blocked)) = // Try to assign to a chat based on In-Reply-To/References. - lookup_chat_by_reply(context, &mime_parser, parent).await? + lookup_chat_by_reply(context, &mime_parser, parent, &is_partial_download) + .await? { // Try to assign to a chat based on In-Reply-To/References. ChatAssignment::ExistingChat { @@ -517,7 +518,7 @@ pub(crate) async fn receive_imf_inner( } else if let Some(parent) = &parent_message { if let Some((chat_id, chat_id_blocked)) = // Try to assign to a chat based on In-Reply-To/References. - lookup_chat_by_reply(context, &mime_parser, parent).await? + lookup_chat_by_reply(context, &mime_parser, parent, &is_partial_download).await? { // Try to assign to a chat based on In-Reply-To/References. ChatAssignment::ExistingChat { @@ -2277,6 +2278,7 @@ async fn lookup_chat_by_reply( context: &Context, mime_parser: &MimeMessage, parent: &Message, + is_partial_download: &Option, ) -> Result> { debug_assert!(mime_parser.get_chat_group_id().is_none()); @@ -2300,7 +2302,10 @@ async fn lookup_chat_by_reply( } // Do not assign unencrypted messages to encrypted chats. - if parent_chat.is_encrypted(context).await? && !mime_parser.was_encrypted() { + if is_partial_download.is_none() + && parent_chat.is_encrypted(context).await? + && !mime_parser.was_encrypted() + { return Ok(None); } From 827e39c026fbc3fe7c86cb70175c10ced34d1a00 Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 26 May 2025 05:19:12 +0000 Subject: [PATCH 325/381] do not lookup adhoc group for 1:1 chat --- src/receive_imf.rs | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index b61c1fa182..f81aee7379 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -530,10 +530,24 @@ pub(crate) async fn receive_imf_inner( } else { ChatAssignment::AdHocGroup } - } else if mime_parser.recipients.len() == 1 { - ChatAssignment::OneOneChat } else { - ChatAssignment::AdHocGroup + let mut num_recipients = mime_parser.recipients.len(); + if from_id != ContactId::SELF { + let mut has_self_addr = false; + for recipient in &mime_parser.recipients { + if context.is_self_addr(&recipient.addr).await? { + has_self_addr = true; + } + } + if !has_self_addr { + num_recipients += 1; + } + } + if num_recipients <= 1 { + ChatAssignment::OneOneChat + } else { + ChatAssignment::AdHocGroup + } }; info!(context, "Chat assignment is {chat_assignment:?}."); @@ -1278,7 +1292,7 @@ async fn do_chat_assignment( chat_id = Some(*new_chat_id); chat_id_blocked = *new_chat_id_blocked; } - ChatAssignment::AdHocGroup | ChatAssignment::OneOneChat => { + ChatAssignment::AdHocGroup => { if let Some((new_chat_id, new_chat_id_blocked)) = lookup_or_create_adhoc_group( context, mime_parser, @@ -1294,6 +1308,7 @@ async fn do_chat_assignment( chat_id_blocked = new_chat_id_blocked; } } + ChatAssignment::OneOneChat => {} } // if the chat is somehow blocked but we want to create a non-blocked chat, From 430cfe3371bec92c5c7a0b97887d077f04b591a3 Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 26 May 2025 07:17:35 +0000 Subject: [PATCH 326/381] cleanup --- src/receive_imf.rs | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index f81aee7379..9397ed62aa 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -481,6 +481,19 @@ pub(crate) async fn receive_imf_inner( // // The chat may not exist yet, i.e. there may be // no database row and ChatId yet. + let mut num_recipients = mime_parser.recipients.len(); + if from_id != ContactId::SELF { + let mut has_self_addr = false; + for recipient in &mime_parser.recipients { + if context.is_self_addr(&recipient.addr).await? { + has_self_addr = true; + } + } + if !has_self_addr { + num_recipients += 1; + } + } + let chat_assignment = if should_trash { ChatAssignment::Trash } else if let Some(grpid) = mime_parser.get_chat_group_id() { @@ -525,24 +538,12 @@ pub(crate) async fn receive_imf_inner( chat_id, chat_id_blocked, } - } else if mime_parser.recipients.len() == 1 { + } else if num_recipients <= 1 { ChatAssignment::OneOneChat } else { ChatAssignment::AdHocGroup } } else { - let mut num_recipients = mime_parser.recipients.len(); - if from_id != ContactId::SELF { - let mut has_self_addr = false; - for recipient in &mime_parser.recipients { - if context.is_self_addr(&recipient.addr).await? { - has_self_addr = true; - } - } - if !has_self_addr { - num_recipients += 1; - } - } if num_recipients <= 1 { ChatAssignment::OneOneChat } else { From 881f8ff772cf314bc7d4e48f8c791dbf965a24ca Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 26 May 2025 07:56:01 +0000 Subject: [PATCH 327/381] do not ignore unencrypted Chat-Group-ID Fix for --- src/mimeparser.rs | 12 ++++++------ src/receive_imf.rs | 12 ++++++++---- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 62921a2a11..f62ec06ff8 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -1529,12 +1529,12 @@ impl MimeMessage { // Chat-Group-ID can only appear in encrypted messages // since PGP-contact migration. // - // Unencrypted ad hoc groups do not have group IDs. - // - // If we receive a Chat-Group-ID header in unencrypted message, - // it is likely sent by old version in opportunistically - // encrypted group that dropped to unencrypted. - remove_header(headers, "chat-group-id", removed); + // However, we do not remove `Chat-Group-ID` here. + // If we receive an unencrypted `Chat-Group-ID` + // from an older version which allowed + // unencrypted groups, we still want to assign + // a message to a group, but it will be an ad hoc group + // without a group ID. // Secure-Join is secured unless it is an initial "vc-request"/"vg-request". if let Some(secure_join) = remove_header(headers, "secure-join", removed) { diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 9397ed62aa..79648f9657 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -2296,7 +2296,11 @@ async fn lookup_chat_by_reply( parent: &Message, is_partial_download: &Option, ) -> Result> { - debug_assert!(mime_parser.get_chat_group_id().is_none()); + // If the message is encrypted and has group ID, + // lookup by reply should never be needed + // as we can directly assign the message to the chat + // by its group ID. + debug_assert!(mime_parser.get_chat_group_id().is_none() || !mime_parser.was_encrypted()); // Try to assign message to the same chat as the parent message. let Some(parent_chat_id) = ChatId::lookup_by_message(parent) else { @@ -2436,9 +2440,9 @@ async fn is_probably_private_reply( parent_chat_id: ChatId, ) -> Result { // Message cannot be a private reply if it has an explicit Chat-Group-ID header. - // - // This function should not even be called in this case. - debug_assert!(mime_parser.get_chat_group_id().is_none()); + if mime_parser.get_chat_group_id().is_some() { + return Ok(false); + } // Usually we don't want to show private replies in the parent chat, but in the // 1:1 chat with the sender. From b3ead8c3735786b5cc64a6ebc8529b8d7ff01813 Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 26 May 2025 09:13:14 +0000 Subject: [PATCH 328/381] clippy --- src/receive_imf.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 79648f9657..65138d9cd7 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -543,12 +543,10 @@ pub(crate) async fn receive_imf_inner( } else { ChatAssignment::AdHocGroup } + } else if num_recipients <= 1 { + ChatAssignment::OneOneChat } else { - if num_recipients <= 1 { - ChatAssignment::OneOneChat - } else { - ChatAssignment::AdHocGroup - } + ChatAssignment::AdHocGroup }; info!(context, "Chat assignment is {chat_assignment:?}."); From 7ddbc199353f72df0dd56d5efdebc835b2a0b2be Mon Sep 17 00:00:00 2001 From: l Date: Thu, 29 May 2025 18:23:24 +0000 Subject: [PATCH 329/381] Update src/receive_imf.rs Co-authored-by: Hocuri --- src/receive_imf.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 65138d9cd7..749010d7f6 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -530,7 +530,6 @@ pub(crate) async fn receive_imf_inner( ChatAssignment::MailingList } else if let Some(parent) = &parent_message { if let Some((chat_id, chat_id_blocked)) = - // Try to assign to a chat based on In-Reply-To/References. lookup_chat_by_reply(context, &mime_parser, parent, &is_partial_download).await? { // Try to assign to a chat based on In-Reply-To/References. From 780afc4a54654fc4ccdd685e0ae3c3c2fb7e9eac Mon Sep 17 00:00:00 2001 From: l Date: Thu, 29 May 2025 18:23:36 +0000 Subject: [PATCH 330/381] Update src/receive_imf.rs Co-authored-by: Hocuri --- src/receive_imf.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 749010d7f6..67342b6b7e 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -435,6 +435,7 @@ pub(crate) async fn receive_imf_inner( .set_config_internal(Config::LastCantDecryptOutgoingMsgs, Some(&now.to_string())) .await?; } + info!(context, "Outgoing undecryptable message (TRASH)."); true } else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage && !mime_parser.has_chat_version() From d72fcb96e7842c9cb96a4a9d38cc1364f3404659 Mon Sep 17 00:00:00 2001 From: l Date: Thu, 29 May 2025 18:23:46 +0000 Subject: [PATCH 331/381] Update src/receive_imf.rs Co-authored-by: Hocuri --- src/receive_imf.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 67342b6b7e..40e1d989bb 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -413,6 +413,7 @@ pub(crate) async fn receive_imf_inner( || mime_parser.get_header(HeaderDef::IrohNodeAddr).is_some() || mime_parser.sync_items.is_some() { + info!(context, "Chat edit/delete/iroh/sync message (TRASH)."); true } else if mime_parser.decrypting_failed && !mime_parser.incoming { // Outgoing undecryptable message. From 6deff155e995fbe1bff5f50aa48f565d9838b550 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 30 May 2025 11:11:08 +0000 Subject: [PATCH 332/381] document ExistingChat --- src/receive_imf.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 40e1d989bb..b160b1b0fd 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -115,8 +115,20 @@ enum ChatAssignment { /// This is not encrypted. AdHocGroup, + /// Assign the message to existing chat + /// with a known `chat_id`. ExistingChat { + /// ID of existing chat + /// which the message should be assigned to. chat_id: ChatId, + + /// Whether existing chat is blocked. + /// This is loaded together with a chat ID + /// reduce the number of database calls. + /// + /// We may want to unblock the chat + /// after adding the message there + /// if the chat is currently blocked. chat_id_blocked: Blocked, }, From eef4c63e140d07b65a65837a4df873a72401242f Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 30 May 2025 11:27:52 +0000 Subject: [PATCH 333/381] Test assigning unencrypted messages with Chat-Group-ID to adhoc groups --- src/mimeparser.rs | 10 -------- src/receive_imf.rs | 6 +++-- src/receive_imf/receive_imf_tests.rs | 34 ++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/src/mimeparser.rs b/src/mimeparser.rs index e44f9fb57a..c307e86a27 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -1526,16 +1526,6 @@ impl MimeMessage { remove_header(headers, "chat-verified", removed); remove_header(headers, "autocrypt-gossip", removed); - // Chat-Group-ID can only appear in encrypted messages - // since PGP-contact migration. - // - // However, we do not remove `Chat-Group-ID` here. - // If we receive an unencrypted `Chat-Group-ID` - // from an older version which allowed - // unencrypted groups, we still want to assign - // a message to a group, but it will be an ad hoc group - // without a group ID. - // Secure-Join is secured unless it is an initial "vc-request"/"vg-request". if let Some(secure_join) = remove_header(headers, "secure-join", removed) { if secure_join == "vc-request" || secure_join == "vg-request" { diff --git a/src/receive_imf.rs b/src/receive_imf.rs index b160b1b0fd..a38c0dab86 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1473,7 +1473,7 @@ async fn do_chat_assignment( ); } } - ChatAssignment::AdHocGroup | ChatAssignment::OneOneChat => { + ChatAssignment::AdHocGroup => { if let Some((new_chat_id, new_chat_id_blocked)) = lookup_or_create_adhoc_group( context, mime_parser, @@ -1489,6 +1489,7 @@ async fn do_chat_assignment( chat_id_blocked = new_chat_id_blocked; } } + ChatAssignment::OneOneChat => {} } if !to_ids.is_empty() { @@ -3323,7 +3324,8 @@ async fn create_adhoc_group( ); return Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not))); } - if member_ids.len() < 3 { + if member_ids.len() < 2 { + info!(context, "Not creating ad hoc group with less than 2 members."); return Ok(None); } diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 0907f5ee72..bee5b93006 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -5115,3 +5115,37 @@ async fn test_no_email_contact_added_into_group() -> Result<()> { Ok(()) } + +/// Tests that message is assigned to an ad hoc group +/// if the message has a `Chat-Group-ID` even +/// if there are only two members in a group. +/// +/// Since PGP-contacts introduction all groups are encrypted, +/// but old versions running on other devices might still +/// create unencrypted groups. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_outgoing_plaintext_two_member_group() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + + let msg = receive_imf( + alice, + b"From: alice@example.org\n\ + To: bob@example.net\n\ + Subject: foo\n\ + Message-ID: \n\ + Chat-Version: 1.0\n\ + Chat-Group-ID: 8ud29aridt29arid\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + Hello\n", + false, + ) + .await? + .unwrap(); + + let chat = Chat::load_from_db(alice, msg.chat_id).await?; + assert_eq!(chat.typ, Chattype::Group); + + Ok(()) +} From f449f08c7ddfc67a5f35a45ba5c56d1672ed6695 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 30 May 2025 13:22:46 +0000 Subject: [PATCH 334/381] remove comment about OutDelivered --- src/receive_imf.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index a38c0dab86..1b515eac49 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1640,8 +1640,6 @@ async fn add_parts( }; let state = if !mime_parser.incoming { - // the mail is on the IMAP server, probably it is also delivered. - // We cannot recreate other states (read, error). MessageState::OutDelivered } else if seen || is_mdn || chat_id_blocked == Blocked::Yes || group_changes.silent // No check for `hidden` because only reactions are such and they should be `InFresh`. From 04554636570a0a6719987d4d5e2894b60b5085d5 Mon Sep 17 00:00:00 2001 From: l Date: Fri, 30 May 2025 13:37:05 +0000 Subject: [PATCH 335/381] Update src/receive_imf.rs Co-authored-by: Hocuri --- src/receive_imf.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 1b515eac49..1bbec80af6 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1535,7 +1535,7 @@ async fn do_chat_assignment( if chat_id_blocked != Blocked::Not { if let Some(chat_id) = chat_id { chat_id.unblock_ex(context, Nosync).await?; - // Not assigning `chat_id_blocked = Blocked::Not` to avoid unused_assignments warning. + chat_id_blocked = Blocked::Not; } } } From 10020f7dcd5ccc819c82dad712a72cf71ad6cad0 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 30 May 2025 13:40:23 +0000 Subject: [PATCH 336/381] adjust GroupChat comment --- src/receive_imf.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 1bbec80af6..b6e2fc2364 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -89,9 +89,7 @@ enum ChatAssignment { /// Group chat with a Group ID. /// /// Lookup PGP contacts and - /// assign to encrypted group - /// even if the message itself - /// is not encrypted. + /// assign to encrypted group. GroupChat { grpid: String }, /// Mailing list or broadcast list. From 4688b0bb6807a931a3c2fdc14bd528a95071ffda Mon Sep 17 00:00:00 2001 From: l Date: Fri, 30 May 2025 13:49:18 +0000 Subject: [PATCH 337/381] Update src/receive_imf.rs Co-authored-by: Hocuri --- src/receive_imf.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index b6e2fc2364..da9cfe6d60 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1199,7 +1199,7 @@ pub async fn from_field_to_contact_id( /// Assigns the message to a chat. /// -/// Creates a new the chat if necessary. +/// Creates a new chat if necessary. #[expect(clippy::too_many_arguments)] async fn do_chat_assignment( context: &Context, From 5bbe5652740a3d16ec2a796d3f7176bc22ab2702 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 30 May 2025 14:06:24 +0000 Subject: [PATCH 338/381] extract decide_chat_assignment --- src/receive_imf.rs | 321 ++++++++++++++++++++++++--------------------- 1 file changed, 172 insertions(+), 149 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index da9cfe6d60..342cb5a355 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -411,154 +411,15 @@ pub(crate) async fn receive_imf_inner( .await? .filter(|p| Some(p.id) != replace_msg_id); - let should_trash = if !mime_parser.mdn_reports.is_empty() { - info!(context, "Message is an MDN (TRASH)."); - true - } else if mime_parser.delivery_report.is_some() { - info!(context, "Message is a DSN (TRASH)."); - markseen_on_imap_table(context, rfc724_mid).await.ok(); - true - } else if mime_parser.get_header(HeaderDef::ChatEdit).is_some() - || mime_parser.get_header(HeaderDef::ChatDelete).is_some() - || mime_parser.get_header(HeaderDef::IrohNodeAddr).is_some() - || mime_parser.sync_items.is_some() - { - info!(context, "Chat edit/delete/iroh/sync message (TRASH)."); - true - } else if mime_parser.decrypting_failed && !mime_parser.incoming { - // Outgoing undecryptable message. - let last_time = context - .get_config_i64(Config::LastCantDecryptOutgoingMsgs) - .await?; - let now = tools::time(); - let update_config = if last_time.saturating_add(24 * 60 * 60) <= now { - let mut msg = Message::new_text(stock_str::cant_decrypt_outgoing_msgs(context).await); - chat::add_device_msg(context, None, Some(&mut msg)) - .await - .log_err(context) - .ok(); - true - } else { - last_time > now - }; - if update_config { - context - .set_config_internal(Config::LastCantDecryptOutgoingMsgs, Some(&now.to_string())) - .await?; - } - info!(context, "Outgoing undecryptable message (TRASH)."); - true - } else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage - && !mime_parser.has_chat_version() - && parent_message - .as_ref() - .is_none_or(|p| p.is_dc_message == MessengerMessage::No) - && !context.get_config_bool(Config::IsChatmail).await? - && ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?) - .unwrap_or_default() - == ShowEmails::Off - { - info!(context, "Classical email not shown (TRASH)."); - // the message is a classic email in a classic profile - // (in chatmail profiles, we always show all messages, because shared dc-mua usage is not supported) - true - } else if mime_parser - .get_header(HeaderDef::XMozillaDraftInfo) - .is_some() - { - // Mozilla Thunderbird does not set \Draft flag on "Templates", but sets - // X-Mozilla-Draft-Info header, which can be used to detect both drafts and templates - // created by Thunderbird. - - // Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them - info!(context, "Email is probably just a draft (TRASH)."); - true - } else if mime_parser.webxdc_status_update.is_some() && mime_parser.parts.len() == 1 { - if let Some(part) = mime_parser.parts.first() { - if part.typ == Viewtype::Text && part.msg.is_empty() { - info!(context, "Message is a status update only (TRASH)."); - markseen_on_imap_table(context, rfc724_mid).await.ok(); - true - } else { - false - } - } else { - false - } - } else { - false - }; - - // Decide on the type of chat we assign the message to. - // - // The chat may not exist yet, i.e. there may be - // no database row and ChatId yet. - let mut num_recipients = mime_parser.recipients.len(); - if from_id != ContactId::SELF { - let mut has_self_addr = false; - for recipient in &mime_parser.recipients { - if context.is_self_addr(&recipient.addr).await? { - has_self_addr = true; - } - } - if !has_self_addr { - num_recipients += 1; - } - } - - let chat_assignment = if should_trash { - ChatAssignment::Trash - } else if let Some(grpid) = mime_parser.get_chat_group_id() { - if mime_parser.was_encrypted() { - ChatAssignment::GroupChat { - grpid: grpid.to_string(), - } - } else if let Some(parent) = &parent_message { - if let Some((chat_id, chat_id_blocked)) = - // Try to assign to a chat based on In-Reply-To/References. - lookup_chat_by_reply(context, &mime_parser, parent, &is_partial_download) - .await? - { - // Try to assign to a chat based on In-Reply-To/References. - ChatAssignment::ExistingChat { - chat_id, - chat_id_blocked, - } - } else { - ChatAssignment::AdHocGroup - } - } else { - // Could be a message from old version - // with opportunistic encryption. - // - // We still want to assign this to a group - // even if it had only two members. - // - // Group ID is ignored, however. - ChatAssignment::AdHocGroup - } - } else if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() { - let _listid = mailinglist_header_listid(mailinglist_header)?; - ChatAssignment::MailingList - } else if let Some(parent) = &parent_message { - if let Some((chat_id, chat_id_blocked)) = - lookup_chat_by_reply(context, &mime_parser, parent, &is_partial_download).await? - { - // Try to assign to a chat based on In-Reply-To/References. - ChatAssignment::ExistingChat { - chat_id, - chat_id_blocked, - } - } else if num_recipients <= 1 { - ChatAssignment::OneOneChat - } else { - ChatAssignment::AdHocGroup - } - } else if num_recipients <= 1 { - ChatAssignment::OneOneChat - } else { - ChatAssignment::AdHocGroup - }; + let chat_assignment = decide_chat_assignment( + context, + &mime_parser, + &parent_message, + &rfc724_mid, + from_id, + &is_partial_download, + ) + .await?; info!(context, "Chat assignment is {chat_assignment:?}."); // ID of the chat to look up the addresses in. @@ -1197,6 +1058,165 @@ pub async fn from_field_to_contact_id( } } +async fn decide_chat_assignment( + context: &Context, + mime_parser: &MimeMessage, + parent_message: &Option, + rfc724_mid: &str, + from_id: ContactId, + is_partial_download: &Option, +) -> Result { + let should_trash = if !mime_parser.mdn_reports.is_empty() { + info!(context, "Message is an MDN (TRASH)."); + true + } else if mime_parser.delivery_report.is_some() { + info!(context, "Message is a DSN (TRASH)."); + markseen_on_imap_table(context, rfc724_mid).await.ok(); + true + } else if mime_parser.get_header(HeaderDef::ChatEdit).is_some() + || mime_parser.get_header(HeaderDef::ChatDelete).is_some() + || mime_parser.get_header(HeaderDef::IrohNodeAddr).is_some() + || mime_parser.sync_items.is_some() + { + info!(context, "Chat edit/delete/iroh/sync message (TRASH)."); + true + } else if mime_parser.decrypting_failed && !mime_parser.incoming { + // Outgoing undecryptable message. + let last_time = context + .get_config_i64(Config::LastCantDecryptOutgoingMsgs) + .await?; + let now = tools::time(); + let update_config = if last_time.saturating_add(24 * 60 * 60) <= now { + let mut msg = Message::new_text(stock_str::cant_decrypt_outgoing_msgs(context).await); + chat::add_device_msg(context, None, Some(&mut msg)) + .await + .log_err(context) + .ok(); + true + } else { + last_time > now + }; + if update_config { + context + .set_config_internal(Config::LastCantDecryptOutgoingMsgs, Some(&now.to_string())) + .await?; + } + info!(context, "Outgoing undecryptable message (TRASH)."); + true + } else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage + && !mime_parser.has_chat_version() + && parent_message + .as_ref() + .is_none_or(|p| p.is_dc_message == MessengerMessage::No) + && !context.get_config_bool(Config::IsChatmail).await? + && ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?) + .unwrap_or_default() + == ShowEmails::Off + { + info!(context, "Classical email not shown (TRASH)."); + // the message is a classic email in a classic profile + // (in chatmail profiles, we always show all messages, because shared dc-mua usage is not supported) + true + } else if mime_parser + .get_header(HeaderDef::XMozillaDraftInfo) + .is_some() + { + // Mozilla Thunderbird does not set \Draft flag on "Templates", but sets + // X-Mozilla-Draft-Info header, which can be used to detect both drafts and templates + // created by Thunderbird. + + // Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them + info!(context, "Email is probably just a draft (TRASH)."); + true + } else if mime_parser.webxdc_status_update.is_some() && mime_parser.parts.len() == 1 { + if let Some(part) = mime_parser.parts.first() { + if part.typ == Viewtype::Text && part.msg.is_empty() { + info!(context, "Message is a status update only (TRASH)."); + markseen_on_imap_table(context, rfc724_mid).await.ok(); + true + } else { + false + } + } else { + false + } + } else { + false + }; + + // Decide on the type of chat we assign the message to. + // + // The chat may not exist yet, i.e. there may be + // no database row and ChatId yet. + let mut num_recipients = mime_parser.recipients.len(); + if from_id != ContactId::SELF { + let mut has_self_addr = false; + for recipient in &mime_parser.recipients { + if context.is_self_addr(&recipient.addr).await? { + has_self_addr = true; + } + } + if !has_self_addr { + num_recipients += 1; + } + } + + let chat_assignment = if should_trash { + ChatAssignment::Trash + } else if let Some(grpid) = mime_parser.get_chat_group_id() { + if mime_parser.was_encrypted() { + ChatAssignment::GroupChat { + grpid: grpid.to_string(), + } + } else if let Some(parent) = &parent_message { + if let Some((chat_id, chat_id_blocked)) = + // Try to assign to a chat based on In-Reply-To/References. + lookup_chat_by_reply(context, &mime_parser, parent, &is_partial_download) + .await? + { + // Try to assign to a chat based on In-Reply-To/References. + ChatAssignment::ExistingChat { + chat_id, + chat_id_blocked, + } + } else { + ChatAssignment::AdHocGroup + } + } else { + // Could be a message from old version + // with opportunistic encryption. + // + // We still want to assign this to a group + // even if it had only two members. + // + // Group ID is ignored, however. + ChatAssignment::AdHocGroup + } + } else if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() { + let _listid = mailinglist_header_listid(mailinglist_header)?; + ChatAssignment::MailingList + } else if let Some(parent) = &parent_message { + if let Some((chat_id, chat_id_blocked)) = + lookup_chat_by_reply(context, &mime_parser, parent, &is_partial_download).await? + { + // Try to assign to a chat based on In-Reply-To/References. + ChatAssignment::ExistingChat { + chat_id, + chat_id_blocked, + } + } else if num_recipients <= 1 { + ChatAssignment::OneOneChat + } else { + ChatAssignment::AdHocGroup + } + } else if num_recipients <= 1 { + ChatAssignment::OneOneChat + } else { + ChatAssignment::AdHocGroup + }; + Ok(chat_assignment) +} + /// Assigns the message to a chat. /// /// Creates a new chat if necessary. @@ -3321,7 +3341,10 @@ async fn create_adhoc_group( return Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not))); } if member_ids.len() < 2 { - info!(context, "Not creating ad hoc group with less than 2 members."); + info!( + context, + "Not creating ad hoc group with less than 2 members." + ); return Ok(None); } From ba267d42c83fcb6410b6c0805a02ee383aa3a0be Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 30 May 2025 14:11:25 +0000 Subject: [PATCH 339/381] clippy --- src/receive_imf.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 342cb5a355..2b5895c590 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -415,7 +415,7 @@ pub(crate) async fn receive_imf_inner( context, &mime_parser, &parent_message, - &rfc724_mid, + rfc724_mid, from_id, &is_partial_download, ) @@ -1171,7 +1171,7 @@ async fn decide_chat_assignment( } else if let Some(parent) = &parent_message { if let Some((chat_id, chat_id_blocked)) = // Try to assign to a chat based on In-Reply-To/References. - lookup_chat_by_reply(context, &mime_parser, parent, &is_partial_download) + lookup_chat_by_reply(context, mime_parser, parent, is_partial_download) .await? { // Try to assign to a chat based on In-Reply-To/References. @@ -1197,7 +1197,7 @@ async fn decide_chat_assignment( ChatAssignment::MailingList } else if let Some(parent) = &parent_message { if let Some((chat_id, chat_id_blocked)) = - lookup_chat_by_reply(context, &mime_parser, parent, &is_partial_download).await? + lookup_chat_by_reply(context, mime_parser, parent, is_partial_download).await? { // Try to assign to a chat based on In-Reply-To/References. ChatAssignment::ExistingChat { From 239174f731a771ebf198b9a530bc5263303b7827 Mon Sep 17 00:00:00 2001 From: l Date: Fri, 30 May 2025 15:23:45 +0000 Subject: [PATCH 340/381] Update src/receive_imf.rs Co-authored-by: Hocuri --- src/receive_imf.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 2b5895c590..e977bcc67c 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1170,7 +1170,6 @@ async fn decide_chat_assignment( } } else if let Some(parent) = &parent_message { if let Some((chat_id, chat_id_blocked)) = - // Try to assign to a chat based on In-Reply-To/References. lookup_chat_by_reply(context, mime_parser, parent, is_partial_download) .await? { From fd225caddd813e47b2863989e2c98314bd53ce8e Mon Sep 17 00:00:00 2001 From: l Date: Fri, 30 May 2025 15:25:15 +0000 Subject: [PATCH 341/381] Update src/receive_imf.rs Co-authored-by: Hocuri --- src/receive_imf.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index e977bcc67c..f6155d5170 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1192,7 +1192,6 @@ async fn decide_chat_assignment( ChatAssignment::AdHocGroup } } else if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() { - let _listid = mailinglist_header_listid(mailinglist_header)?; ChatAssignment::MailingList } else if let Some(parent) = &parent_message { if let Some((chat_id, chat_id_blocked)) = From 67add91460d8408f7d8b58d197c9cf3024cddb23 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 30 May 2025 15:30:32 +0000 Subject: [PATCH 342/381] lint/fmt --- src/receive_imf.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index f6155d5170..4d1311042f 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1170,8 +1170,7 @@ async fn decide_chat_assignment( } } else if let Some(parent) = &parent_message { if let Some((chat_id, chat_id_blocked)) = - lookup_chat_by_reply(context, mime_parser, parent, is_partial_download) - .await? + lookup_chat_by_reply(context, mime_parser, parent, is_partial_download).await? { // Try to assign to a chat based on In-Reply-To/References. ChatAssignment::ExistingChat { @@ -1191,7 +1190,7 @@ async fn decide_chat_assignment( // Group ID is ignored, however. ChatAssignment::AdHocGroup } - } else if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() { + } else if mime_parser.get_mailinglist_header().is_some() { ChatAssignment::MailingList } else if let Some(parent) = &parent_message { if let Some((chat_id, chat_id_blocked)) = From 8774505ef0fe215e240c78707383c052874f6b31 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Wed, 4 Jun 2025 15:08:09 +0200 Subject: [PATCH 343/381] perf: Cache self fingerprint instead of loading it every time (#6865) Targets https://github.com/chatmail/core/pull/6796#discussion_r2051799171 Right now, `self_fingerprint` returns the fingerprint's hex representation as a `String`; alternatively, it could return a `Fingerprint`. While one could argue for both sides, I don't think it matters much; if you would like, I can change it. --- src/contact.rs | 9 +++++---- src/context.rs | 14 +++++++++---- src/key.rs | 32 ++++++++++++++++++++++++++++++ src/mimefactory.rs | 9 ++++----- src/mimeparser.rs | 2 +- src/receive_imf.rs | 6 +++--- src/securejoin/bob.rs | 6 +++--- src/securejoin/securejoin_tests.rs | 16 +++++++-------- src/test_utils.rs | 18 ++++++----------- src/webxdc.rs | 4 ++-- 10 files changed, 73 insertions(+), 43 deletions(-) diff --git a/src/contact.rs b/src/contact.rs index a718f88f7e..08e0aeea2a 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -28,7 +28,8 @@ use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF}; use crate::context::Context; use crate::events::EventType; use crate::key::{ - load_self_public_key, load_self_public_key_opt, DcKey, Fingerprint, SignedPublicKey, + load_self_public_key, self_fingerprint, self_fingerprint_opt, DcKey, Fingerprint, + SignedPublicKey, }; use crate::log::LogExt; use crate::message::MessageState; @@ -623,8 +624,8 @@ impl Contact { .get_config(Config::ConfiguredAddr) .await? .unwrap_or_default(); - if let Some(public_key) = load_self_public_key_opt(context).await? { - contact.fingerprint = Some(public_key.dc_fingerprint().hex()); + if let Some(self_fp) = self_fingerprint_opt(context).await? { + contact.fingerprint = Some(self_fp.to_string()); } contact.status = context .get_config(Config::Selfstatus) @@ -855,7 +856,7 @@ impl Contact { } if !fingerprint.is_empty() { - let fingerprint_self = load_self_public_key(context).await?.dc_fingerprint().hex(); + let fingerprint_self = self_fingerprint(context).await?; if fingerprint == fingerprint_self { return Ok((ContactId::SELF, sth_modified)); } diff --git a/src/context.rs b/src/context.rs index 9fb279843a..be9ca15303 100644 --- a/src/context.rs +++ b/src/context.rs @@ -5,7 +5,7 @@ use std::ffi::OsString; use std::ops::Deref; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; +use std::sync::{Arc, OnceLock}; use std::time::Duration; use anyhow::{bail, ensure, Context as _, Result}; @@ -25,7 +25,7 @@ use crate::debug_logging::DebugLogging; use crate::download::DownloadState; use crate::events::{Event, EventEmitter, EventType, Events}; use crate::imap::{FolderMeaning, Imap, ServerMetadata}; -use crate::key::{load_self_public_key, load_self_secret_key, DcKey as _}; +use crate::key::{load_self_secret_key, self_fingerprint}; use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam}; use crate::message::{self, Message, MessageState, MsgId}; use crate::param::{Param, Params}; @@ -297,6 +297,11 @@ pub struct InnerContext { /// Iroh for realtime peer channels. pub(crate) iroh: Arc>>, + + /// The own fingerprint, if it was computed already. + /// tokio::sync::OnceCell would be possible to use, but overkill for our usecase; + /// the standard library's OnceLock is enough, and it's a lot smaller in memory. + pub(crate) self_fingerprint: OnceLock, } /// The state of ongoing process. @@ -456,6 +461,7 @@ impl Context { push_subscriber, push_subscribed: AtomicBool::new(false), iroh: Arc::new(RwLock::new(None)), + self_fingerprint: OnceLock::new(), }; let ctx = Context { @@ -813,8 +819,8 @@ impl Context { .sql .count("SELECT COUNT(*) FROM public_keys;", ()) .await?; - let fingerprint_str = match load_self_public_key(self).await { - Ok(key) => key.dc_fingerprint().hex(), + let fingerprint_str = match self_fingerprint(self).await { + Ok(fp) => fp.to_string(), Err(err) => format!(""), }; diff --git a/src/key.rs b/src/key.rs index d555a9d93b..77c7ed616e 100644 --- a/src/key.rs +++ b/src/key.rs @@ -185,6 +185,38 @@ pub(crate) async fn load_self_public_keyring(context: &Context) -> Result Result<&str> { + if let Some(fp) = context.self_fingerprint.get() { + Ok(fp) + } else { + let fp = load_self_public_key(context).await?.dc_fingerprint().hex(); + Ok(context.self_fingerprint.get_or_init(|| fp)) + } +} + +/// Returns own public key fingerprint in (not human-readable) hex representation. +/// This is the fingerprint format that is used in the database. +/// +/// Returns `None` if no key is generated yet. +/// +/// For performance reasons, the fingerprint is cached after the first invocation. +pub(crate) async fn self_fingerprint_opt(context: &Context) -> Result> { + if let Some(fp) = context.self_fingerprint.get() { + Ok(Some(fp)) + } else if let Some(key) = load_self_public_key_opt(context).await? { + let fp = key.dc_fingerprint().hex(); + Ok(Some(context.self_fingerprint.get_or_init(|| fp))) + } else { + Ok(None) + } +} + pub(crate) async fn load_self_secret_key(context: &Context) -> Result { let private_key = context .sql diff --git a/src/mimefactory.rs b/src/mimefactory.rs index acbc823cc4..8b895d969a 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -23,7 +23,7 @@ use crate::contact::{Contact, ContactId, Origin}; use crate::context::Context; use crate::e2ee::EncryptHelper; use crate::ephemeral::Timer as EphemeralTimer; -use crate::key::load_self_public_key; +use crate::key::self_fingerprint; use crate::key::{DcKey, SignedPublicKey}; use crate::location; use crate::message::{self, Message, MsgId, Viewtype}; @@ -205,8 +205,7 @@ impl MimeFactory { let encryption_keys; - let self_public_key = load_self_public_key(context).await?; - let self_fingerprint = self_public_key.dc_fingerprint().hex(); + let self_fingerprint = self_fingerprint(context).await?; if chat.is_self_talk() { to.push((from_displayname.to_string(), from_addr.to_string())); @@ -312,7 +311,7 @@ impl MimeFactory { if !fingerprint.is_empty() { member_fingerprints.push(fingerprint); } else if id == ContactId::SELF { - member_fingerprints.push(self_fingerprint.clone()); + member_fingerprints.push(self_fingerprint.to_string()); } else { debug_assert!(member_fingerprints.is_empty(), "If some past member is a PGP-contact, all other past members should be PGP-contacts too"); } @@ -363,7 +362,7 @@ impl MimeFactory { } else if id == ContactId::SELF { // It's fine to have self in past members // if we are leaving the group. - past_member_fingerprints.push(self_fingerprint.clone()); + past_member_fingerprints.push(self_fingerprint.to_string()); } else { debug_assert!(past_member_fingerprints.is_empty(), "If some past member is a PGP-contact, all other past members should be PGP-contacts too"); } diff --git a/src/mimeparser.rs b/src/mimeparser.rs index c307e86a27..613b848c91 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -255,7 +255,7 @@ impl MimeMessage { ); headers.retain(|k, _| { !is_hidden(k) || { - headers_removed.insert(k.clone()); + headers_removed.insert(k.to_string()); false } }); diff --git a/src/receive_imf.rs b/src/receive_imf.rs index ead10aabe3..3b13f4595b 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -25,7 +25,7 @@ use crate::ephemeral::{stock_ephemeral_timer_changed, Timer as EphemeralTimer}; use crate::events::EventType; use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::imap::{markseen_on_imap_table, GENERATED_PREFIX}; -use crate::key::load_self_public_key_opt; +use crate::key::self_fingerprint_opt; use crate::key::{DcKey, Fingerprint, SignedPublicKey}; use crate::log::LogExt; use crate::message::{ @@ -3655,8 +3655,8 @@ async fn lookup_pgp_contact_by_fingerprint( .await? { Ok(Some(contact_id)) - } else if let Some(self_public_key) = load_self_public_key_opt(context).await? { - if self_public_key.dc_fingerprint().hex() == fingerprint { + } else if let Some(self_fp) = self_fingerprint_opt(context).await? { + if self_fp == fingerprint { Ok(Some(ContactId::SELF)) } else { Ok(None) diff --git a/src/securejoin/bob.rs b/src/securejoin/bob.rs index fb3a840f9a..f5c52aa691 100644 --- a/src/securejoin/bob.rs +++ b/src/securejoin/bob.rs @@ -9,7 +9,7 @@ use crate::constants::{Blocked, Chattype}; use crate::contact::Origin; use crate::context::Context; use crate::events::EventType; -use crate::key::{load_self_public_key, DcKey}; +use crate::key::self_fingerprint; use crate::message::{Message, Viewtype}; use crate::mimeparser::{MimeMessage, SystemMessage}; use crate::param::Param; @@ -273,8 +273,8 @@ pub(crate) async fn send_handshake_message( msg.param.set_int(Param::GuaranteeE2ee, 1); // Sends our own fingerprint in the Secure-Join-Fingerprint header. - let bob_fp = load_self_public_key(context).await?.dc_fingerprint(); - msg.param.set(Param::Arg3, bob_fp.hex()); + let bob_fp = self_fingerprint(context).await?; + msg.param.set(Param::Arg3, bob_fp); // Sends the grpid in the Secure-Join-Group header. // diff --git a/src/securejoin/securejoin_tests.rs b/src/securejoin/securejoin_tests.rs index d565fe9f0a..b33f6b66be 100644 --- a/src/securejoin/securejoin_tests.rs +++ b/src/securejoin/securejoin_tests.rs @@ -4,6 +4,7 @@ use super::*; use crate::chat::{remove_contact_from_chat, CantSendReason}; use crate::chatlist::Chatlist; use crate::constants::Chattype; +use crate::key::self_fingerprint; use crate::receive_imf::receive_imf; use crate::stock_str::{self, chat_protection_enabled}; use crate::test_utils::{ @@ -177,13 +178,10 @@ async fn test_setup_contact_ex(case: SetupContactCase) { "vc-request-with-auth" ); assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some()); - let bob_fp = load_self_public_key(&bob.ctx) - .await - .unwrap() - .dc_fingerprint(); + let bob_fp = self_fingerprint(&bob).await.unwrap(); assert_eq!( - *msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(), - bob_fp.hex() + msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(), + bob_fp ); if case == SetupContactCase::WrongAliceGossip { @@ -498,10 +496,10 @@ async fn test_secure_join() -> Result<()> { "vg-request-with-auth" ); assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some()); - let bob_fp = load_self_public_key(&bob).await?.dc_fingerprint(); + let bob_fp = self_fingerprint(&bob).await?; assert_eq!( - *msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(), - bob_fp.hex() + msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(), + bob_fp ); // Alice should not yet have Bob verified diff --git a/src/test_utils.rs b/src/test_utils.rs index 23e1cab992..e403ca7e11 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -32,7 +32,7 @@ use crate::contact::{ }; use crate::context::Context; use crate::events::{Event, EventEmitter, EventType, Events}; -use crate::key::{self, load_self_public_key, DcKey, DcSecretKey}; +use crate::key::{self, self_fingerprint, DcKey, DcSecretKey}; use crate::message::{update_msg_state, Message, MessageState, MsgId, Viewtype}; use crate::mimeparser::{MimeMessage, SystemMessage}; use crate::pgp::KeyPair; @@ -770,18 +770,12 @@ impl TestContext { pub async fn add_or_lookup_pgp_contact(&self, other: &TestContext) -> Contact { let primary_self_addr = other.ctx.get_primary_self_addr().await.unwrap(); let addr = ContactAddress::new(&primary_self_addr).unwrap(); - let public_key = load_self_public_key(other).await.unwrap(); - let fingerprint = public_key.dc_fingerprint(); + let fingerprint = self_fingerprint(other).await.unwrap(); - let (contact_id, _modified) = Contact::add_or_lookup_ex( - self, - "", - &addr, - &fingerprint.hex(), - Origin::MailinglistAddress, - ) - .await - .expect("add_or_lookup"); + let (contact_id, _modified) = + Contact::add_or_lookup_ex(self, "", &addr, fingerprint, Origin::MailinglistAddress) + .await + .expect("add_or_lookup"); Contact::get_by_id(&self.ctx, contact_id).await.unwrap() } diff --git a/src/webxdc.rs b/src/webxdc.rs index ecc4d58f97..df96e9937d 100644 --- a/src/webxdc.rs +++ b/src/webxdc.rs @@ -39,7 +39,7 @@ use crate::constants::Chattype; use crate::contact::ContactId; use crate::context::Context; use crate::events::EventType; -use crate::key::{load_self_public_key, DcKey}; +use crate::key::self_fingerprint; use crate::message::{Message, MessageState, MsgId, Viewtype}; use crate::mimefactory::RECOMMENDED_FILE_SIZE; use crate::mimeparser::SystemMessage; @@ -962,7 +962,7 @@ impl Message { } async fn get_webxdc_self_addr(&self, context: &Context) -> Result { - let fingerprint = load_self_public_key(context).await?.dc_fingerprint().hex(); + let fingerprint = self_fingerprint(context).await?; let data = format!("{}-{}", fingerprint, self.rfc724_mid); let hash = Sha256::digest(data.as_bytes()); Ok(format!("{hash:x}")) From e4cb2f4333cf38dc329572f2db61bda3cb71c39e Mon Sep 17 00:00:00 2001 From: l Date: Thu, 5 Jun 2025 20:51:51 +0000 Subject: [PATCH 344/381] Update deltachat-jsonrpc/src/api/types/chat_list.rs Co-authored-by: Hocuri --- deltachat-jsonrpc/src/api/types/chat_list.rs | 28 ++++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/deltachat-jsonrpc/src/api/types/chat_list.rs b/deltachat-jsonrpc/src/api/types/chat_list.rs index 802dc9c00a..90f9a7eaa3 100644 --- a/deltachat-jsonrpc/src/api/types/chat_list.rs +++ b/deltachat-jsonrpc/src/api/types/chat_list.rs @@ -31,11 +31,29 @@ pub enum ChatListItemFetchResult { summary_preview_image: Option, is_protected: bool, - /// True if the chat is encrypted. - /// - /// False e.g. for 1:1 chats with email-contacts - /// and ad hoc groups. - is_encrypted: bool, + /// True if the chat is encrypted. + /// This means that all messages in the chat are encrypted, + /// and all contacts in the chat are "pgp-contacts", + /// i.e. identified by the PGP key fingerprint. + /// + /// False if the chat is unencrypted. + /// This means that all messages in the chat are unencrypted, + /// and all contacts in the chat are "email-contacts", + /// i.e. identified by the email address. + /// The UI should mark this chat e.g. with a mail-letter icon. + /// + /// Unencrypted groups are called "ad-hoc groups" + /// and the user can't add/remove members, + /// create a QR invite code, + /// or set an avatar. + /// These options should therefore be disabled in the UI. + /// + /// Note that it can happen that an encrypted chat + /// contains unencrypted messages that were received in core <= v1.159.* + /// and vice versa. + /// + /// See also `is_pgp_contact` on `Contact`. + is_encrypted: bool, is_group: bool, fresh_message_counter: usize, is_self_talk: bool, From f920d67b99fd2d9e086cb7992d78b19461d23bb0 Mon Sep 17 00:00:00 2001 From: l Date: Thu, 5 Jun 2025 20:52:03 +0000 Subject: [PATCH 345/381] Update deltachat-jsonrpc/src/api/types/chat.rs Co-authored-by: Hocuri --- deltachat-jsonrpc/src/api/types/chat.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/deltachat-jsonrpc/src/api/types/chat.rs b/deltachat-jsonrpc/src/api/types/chat.rs index 2d22b101a4..5fb361b0a6 100644 --- a/deltachat-jsonrpc/src/api/types/chat.rs +++ b/deltachat-jsonrpc/src/api/types/chat.rs @@ -30,6 +30,28 @@ pub struct FullChat { /// in the contact profile /// if 1:1 chat with this contact exists and is protected. is_protected: bool, + /// True if the chat is encrypted. + /// This means that all messages in the chat are encrypted, + /// and all contacts in the chat are "pgp-contacts", + /// i.e. identified by the PGP key fingerprint. + /// + /// False if the chat is unencrypted. + /// This means that all messages in the chat are unencrypted, + /// and all contacts in the chat are "email-contacts", + /// i.e. identified by the email address. + /// The UI should mark this chat e.g. with a mail-letter icon. + /// + /// Unencrypted groups are called "ad-hoc groups" + /// and the user can't add/remove members, + /// create a QR invite code, + /// or set an avatar. + /// These options should therefore be disabled in the UI. + /// + /// Note that it can happen that an encrypted chat + /// contains unencrypted messages that were received in core <= v1.159.* + /// and vice versa. + /// + /// See also `is_pgp_contact` on `Contact`. is_encrypted: bool, profile_image: Option, //BLOBS ? archived: bool, From 70ce56cc523e80839d322e46cb596d4bf0e750c7 Mon Sep 17 00:00:00 2001 From: l Date: Thu, 5 Jun 2025 20:52:19 +0000 Subject: [PATCH 346/381] Update deltachat-jsonrpc/src/api/types/chat.rs Co-authored-by: Hocuri --- deltachat-jsonrpc/src/api/types/chat.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/deltachat-jsonrpc/src/api/types/chat.rs b/deltachat-jsonrpc/src/api/types/chat.rs index 5fb361b0a6..3e90d63b24 100644 --- a/deltachat-jsonrpc/src/api/types/chat.rs +++ b/deltachat-jsonrpc/src/api/types/chat.rs @@ -184,6 +184,28 @@ pub struct BasicChat { /// if 1:1 chat with this contact exists and is protected. is_protected: bool, + /// True if the chat is encrypted. + /// This means that all messages in the chat are encrypted, + /// and all contacts in the chat are "pgp-contacts", + /// i.e. identified by the PGP key fingerprint. + /// + /// False if the chat is unencrypted. + /// This means that all messages in the chat are unencrypted, + /// and all contacts in the chat are "email-contacts", + /// i.e. identified by the email address. + /// The UI should mark this chat e.g. with a mail-letter icon. + /// + /// Unencrypted groups are called "ad-hoc groups" + /// and the user can't add/remove members, + /// create a QR invite code, + /// or set an avatar. + /// These options should therefore be disabled in the UI. + /// + /// Note that it can happen that an encrypted chat + /// contains unencrypted messages that were received in core <= v1.159.* + /// and vice versa. + /// + /// See also `is_pgp_contact` on `Contact`. is_encrypted: bool, profile_image: Option, //BLOBS ? archived: bool, From b93ce5aa50c97bb4dc7cccc5fbef9d8a23fd508c Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 5 Jun 2025 21:51:57 +0000 Subject: [PATCH 347/381] deprecate DC_STR_E2E_PREFERRED --- deltachat-ffi/deltachat.h | 1 + src/stock_str.rs | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 6b86d499e9..a8b12e8f0d 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -6901,6 +6901,7 @@ void dc_event_unref(dc_event_t* event); /// "End-to-end encryption preferred." /// /// Used to build the string returned by dc_get_contact_encrinfo(). +/// @deprecated 2025-06-05 #define DC_STR_E2E_PREFERRED 34 /// "%1$s verified" diff --git a/src/stock_str.rs b/src/stock_str.rs index 255e05b595..9b06d47dd4 100644 --- a/src/stock_str.rs +++ b/src/stock_str.rs @@ -80,9 +80,6 @@ pub enum StockMessage { #[strum(props(fallback = "Fingerprints"))] FingerPrints = 30, - #[strum(props(fallback = "End-to-end encryption preferred"))] - E2ePreferred = 34, - #[strum(props(fallback = "%1$s verified."))] ContactVerified = 35, From 54457123c30122ff521531d22a2d7801ca4c87ab Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 5 Jun 2025 21:53:25 +0000 Subject: [PATCH 348/381] deprecate more strings --- deltachat-ffi/deltachat.h | 4 ++++ src/stock_str.rs | 14 -------------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index a8b12e8f0d..53891ab08e 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -6914,12 +6914,14 @@ void dc_event_unref(dc_event_t* event); /// /// Used in status messages. /// - %1$s will be replaced by the name of the contact that cannot be verified +/// @deprecated 2025-06-05 #define DC_STR_CONTACT_NOT_VERIFIED 36 /// "Changed setup for %1$s." /// /// Used in status messages. /// - %1$s will be replaced by the name of the contact with the changed setup +/// @deprecated 2025-06-05 #define DC_STR_CONTACT_SETUP_CHANGED 37 /// "Archived chats" @@ -7309,6 +7311,7 @@ void dc_event_unref(dc_event_t* event); /// "%1$s changed their address from %2$s to %3$s" /// /// Used as an info message to chats with contacts that changed their address. +/// @deprecated 2025-06-05 #define DC_STR_AEAP_ADDR_CHANGED 122 /// "You changed your email address from %1$s to %2$s. @@ -7615,6 +7618,7 @@ void dc_event_unref(dc_event_t* event); /// "The contact must be online to proceed. This process will continue automatically in background." /// /// Used as info message. +/// @deprecated 2025-06-05 #define DC_STR_SECUREJOIN_TAKES_LONGER 192 /// "Contact". Deprecated, currently unused. diff --git a/src/stock_str.rs b/src/stock_str.rs index 9b06d47dd4..ead552d94c 100644 --- a/src/stock_str.rs +++ b/src/stock_str.rs @@ -83,12 +83,6 @@ pub enum StockMessage { #[strum(props(fallback = "%1$s verified."))] ContactVerified = 35, - #[strum(props(fallback = "Cannot establish guaranteed end-to-end encryption with %1$s"))] - ContactNotVerified = 36, - - #[strum(props(fallback = "Changed setup for %1$s"))] - ContactSetupChanged = 37, - #[strum(props(fallback = "Archived chats"))] ArchivedChats = 40, @@ -265,9 +259,6 @@ pub enum StockMessage { #[strum(props(fallback = "Not connected"))] NotConnected = 121, - #[strum(props(fallback = "%1$s changed their address from %2$s to %3$s"))] - AeapAddrChanged = 122, - #[strum(props( fallback = "You changed your email address from %1$s to %2$s.\n\nIf you now send a message to a verified group, contacts there will automatically replace the old with your new address.\n\nIt's highly advised to set up your old email provider to forward all emails to your new email address. Otherwise you might miss messages of contacts who did not get your new address yet." ))] @@ -425,11 +416,6 @@ pub enum StockMessage { #[strum(props(fallback = "Establishing guaranteed end-to-end encryption, please wait…"))] SecurejoinWait = 190, - - #[strum(props( - fallback = "The contact must be online to proceed.\n\nThis process will continue automatically in background." - ))] - SecurejoinTakesLonger = 192, } impl StockMessage { From 4aeb2a7ad1782b1712a9829f648c77df154a1b98 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 5 Jun 2025 22:08:31 +0000 Subject: [PATCH 349/381] rustfmt --- deltachat-jsonrpc/src/api/types/chat_list.rs | 46 ++++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/deltachat-jsonrpc/src/api/types/chat_list.rs b/deltachat-jsonrpc/src/api/types/chat_list.rs index 90f9a7eaa3..285de89f81 100644 --- a/deltachat-jsonrpc/src/api/types/chat_list.rs +++ b/deltachat-jsonrpc/src/api/types/chat_list.rs @@ -31,29 +31,29 @@ pub enum ChatListItemFetchResult { summary_preview_image: Option, is_protected: bool, - /// True if the chat is encrypted. - /// This means that all messages in the chat are encrypted, - /// and all contacts in the chat are "pgp-contacts", - /// i.e. identified by the PGP key fingerprint. - /// - /// False if the chat is unencrypted. - /// This means that all messages in the chat are unencrypted, - /// and all contacts in the chat are "email-contacts", - /// i.e. identified by the email address. - /// The UI should mark this chat e.g. with a mail-letter icon. - /// - /// Unencrypted groups are called "ad-hoc groups" - /// and the user can't add/remove members, - /// create a QR invite code, - /// or set an avatar. - /// These options should therefore be disabled in the UI. - /// - /// Note that it can happen that an encrypted chat - /// contains unencrypted messages that were received in core <= v1.159.* - /// and vice versa. - /// - /// See also `is_pgp_contact` on `Contact`. - is_encrypted: bool, + /// True if the chat is encrypted. + /// This means that all messages in the chat are encrypted, + /// and all contacts in the chat are "pgp-contacts", + /// i.e. identified by the PGP key fingerprint. + /// + /// False if the chat is unencrypted. + /// This means that all messages in the chat are unencrypted, + /// and all contacts in the chat are "email-contacts", + /// i.e. identified by the email address. + /// The UI should mark this chat e.g. with a mail-letter icon. + /// + /// Unencrypted groups are called "ad-hoc groups" + /// and the user can't add/remove members, + /// create a QR invite code, + /// or set an avatar. + /// These options should therefore be disabled in the UI. + /// + /// Note that it can happen that an encrypted chat + /// contains unencrypted messages that were received in core <= v1.159.* + /// and vice versa. + /// + /// See also `is_pgp_contact` on `Contact`. + is_encrypted: bool, is_group: bool, fresh_message_counter: usize, is_self_talk: bool, From 67fea3a1d3712bb8ef697ae56f116285375fbf98 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 7 Jun 2025 20:40:35 +0000 Subject: [PATCH 350/381] rename add_or_lookup_pgp_contact into add_or_lookup_contact_no_key --- src/securejoin/securejoin_tests.rs | 20 ++++++++++---------- src/test_utils.rs | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/securejoin/securejoin_tests.rs b/src/securejoin/securejoin_tests.rs index b33f6b66be..101c9e5d52 100644 --- a/src/securejoin/securejoin_tests.rs +++ b/src/securejoin/securejoin_tests.rs @@ -108,7 +108,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) { bob_chat.why_cant_send(&bob).await.unwrap(), Some(CantSendReason::MissingKey) ); - let contact_alice_id = bob.add_or_lookup_pgp_contact(&alice).await.id; + let contact_alice_id = bob.add_or_lookup_contact_no_key(&alice).await.id; let sent = bob.pop_sent_msg().await; assert!(!sent.payload.contains("Bob Examplenet")); assert_eq!(sent.recipient(), EmailAddress::new(alice_addr).unwrap()); @@ -190,7 +190,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) { .gossiped_keys .insert(alice_addr.to_string(), wrong_pubkey) .unwrap(); - let contact_bob = alice.add_or_lookup_pgp_contact(&bob).await; + let contact_bob = alice.add_or_lookup_contact_no_key(&bob).await; let handshake_msg = handle_securejoin_handshake(&alice, &mut msg, contact_bob.id) .await .unwrap(); @@ -209,7 +209,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) { } // Alice should not yet have Bob verified - let contact_bob = alice.add_or_lookup_pgp_contact(&bob).await; + let contact_bob = alice.add_or_lookup_contact_no_key(&bob).await; let contact_bob_id = contact_bob.id; assert_eq!(contact_bob.is_pgp_contact(), true); assert_eq!(contact_bob.is_verified(&alice).await.unwrap(), false); @@ -354,7 +354,7 @@ async fn test_setup_contact_bob_knows_alice() -> Result<()> { ); // Alice should not yet have Bob verified - let contact_bob = alice.add_or_lookup_pgp_contact(bob).await; + let contact_bob = alice.add_or_lookup_contact_no_key(bob).await; assert_eq!(contact_bob.is_verified(alice).await?, false); tcm.section("Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm"); @@ -370,7 +370,7 @@ async fn test_setup_contact_bob_knows_alice() -> Result<()> { ); // Bob has verified Alice already. - let contact_alice = bob.add_or_lookup_pgp_contact(alice).await; + let contact_alice = bob.add_or_lookup_contact_no_key(alice).await; assert_eq!(contact_alice.is_verified(bob).await?, true); // Alice confirms that Bob is now verified. @@ -469,7 +469,7 @@ async fn test_secure_join() -> Result<()> { bob.recv_msg_trash(&sent).await; let sent = bob.pop_sent_msg().await; - let contact_alice_id = bob.add_or_lookup_pgp_contact(&alice).await.id; + let contact_alice_id = bob.add_or_lookup_contact_no_key(&alice).await.id; // Check Bob emitted the JoinerProgress event. let event = bob @@ -503,7 +503,7 @@ async fn test_secure_join() -> Result<()> { ); // Alice should not yet have Bob verified - let contact_bob = alice.add_or_lookup_pgp_contact(&bob).await; + let contact_bob = alice.add_or_lookup_contact_no_key(&bob).await; assert_eq!(contact_bob.is_verified(&alice).await?, false); tcm.section("Step 5+6: Alice receives vg-request-with-auth, sends vg-member-added"); @@ -546,7 +546,7 @@ async fn test_secure_join() -> Result<()> { // Bob has verified Alice already. // // Alice may not have verified Bob yet. - let contact_alice = bob.add_or_lookup_pgp_contact(&alice).await; + let contact_alice = bob.add_or_lookup_contact_no_key(&alice).await; assert_eq!(contact_alice.is_verified(&bob).await?, true); tcm.section("Step 7: Bob receives vg-member-added"); @@ -670,14 +670,14 @@ async fn test_lost_contact_confirm() { alice.recv_msg_trash(&sent).await; // Alice has Bob verified now. - let contact_bob = alice.add_or_lookup_pgp_contact(&bob).await; + let contact_bob = alice.add_or_lookup_contact_no_key(&bob).await; assert_eq!(contact_bob.is_verified(&alice).await.unwrap(), true); // Alice sends vc-contact-confirm, but it gets lost. let _sent_vc_contact_confirm = alice.pop_sent_msg().await; // Bob has alice as verified too, even though vc-contact-confirm is lost. - let contact_alice = bob.add_or_lookup_pgp_contact(&alice).await; + let contact_alice = bob.add_or_lookup_contact_no_key(&alice).await; assert_eq!(contact_alice.is_verified(&bob).await.unwrap(), true); } diff --git a/src/test_utils.rs b/src/test_utils.rs index e403ca7e11..608a80d196 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -767,7 +767,7 @@ impl TestContext { /// /// If the contact does not exist yet, a new contact will be created /// with the correct fingerprint, but without the public key. - pub async fn add_or_lookup_pgp_contact(&self, other: &TestContext) -> Contact { + pub async fn add_or_lookup_contact_no_key(&self, other: &TestContext) -> Contact { let primary_self_addr = other.ctx.get_primary_self_addr().await.unwrap(); let addr = ContactAddress::new(&primary_self_addr).unwrap(); let fingerprint = self_fingerprint(other).await.unwrap(); From d811d7d2fe4de561ceb0facc0853c3fbf25d373c Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 7 Jun 2025 21:19:37 +0000 Subject: [PATCH 351/381] Rename is_partial_download into find_pgp_contact_by_addr --- src/receive_imf.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 3b13f4595b..2ca5133f51 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -968,9 +968,10 @@ pub(crate) async fn receive_imf_inner( /// display names. We don't want the display name to change every time the user gets a new email from /// a mailing list. /// -/// * `is_partial_download`: the message is partially downloaded. -/// We only know the email address and not the contact fingerprint, -/// but try to assign the message to some PGP-contact. +/// * `find_pgp_contact_by_addr`: if true, we only know the e-mail address +/// of the contact, but not the fingerprint, +/// yet want to assign the message to some PGP-contact. +/// This can happen during prefetch or when the message is partially downloaded. /// If we get it wrong, the message will be placed into the correct /// chat after downloading. /// @@ -980,7 +981,7 @@ pub async fn from_field_to_contact_id( from: &SingleInfo, fingerprint: Option<&Fingerprint>, prevent_rename: bool, - is_partial_download: bool, + find_pgp_contact_by_addr: bool, ) -> Result> { let fingerprint = fingerprint.as_ref().map(|fp| fp.hex()).unwrap_or_default(); let display_name = if prevent_rename { @@ -999,7 +1000,7 @@ pub async fn from_field_to_contact_id( } }; - if fingerprint.is_empty() && is_partial_download { + if fingerprint.is_empty() && find_pgp_contact_by_addr { let addr_normalized = addr_normalize(&from_addr); // Try to assign to some PGP-contact. From 7b3252395deb0490663751001042e73e9e3f66c0 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 8 Jun 2025 14:58:41 +0000 Subject: [PATCH 352/381] test --- src/receive_imf/receive_imf_tests.rs | 35 ++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index bee5b93006..6f2663b639 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -5149,3 +5149,38 @@ async fn test_outgoing_plaintext_two_member_group() -> Result<()> { Ok(()) } + +/// Tests that large messages are assigned +/// to non-PGP contacts if the type is not `multipart/encrypted`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_partial_download_pgp_contact_lookup() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + // Create two chats with Alice, both with PGP-contact and email address contact. + let encrypted_chat = bob.create_chat(alice).await; + let unencrypted_chat = bob.create_email_chat(alice).await; + + let seen = false; + let is_partial_download = Some(9999); + let received = receive_imf_from_inbox( + bob, + "3333@example.org", + b"From: alice@example.org\n\ + To: bob@example.net\n\ + Message-ID: <3333@example.org>\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", + seen, + is_partial_download, + ) + .await? + .unwrap(); + + assert_ne!(received.chat_id, encrypted_chat.id); + assert_eq!(received.chat_id, unencrypted_chat.id); + + Ok(()) +} From 8adce32594c99d7eea1bde2af5551e29e4b2eecd Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 7 Jun 2025 21:19:37 +0000 Subject: [PATCH 353/381] Lookup PGP contacts only for multipart/encrypted messages --- src/receive_imf.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 2ca5133f51..ec61577fc1 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -379,7 +379,11 @@ pub(crate) async fn receive_imf_inner( &mime_parser.from, fingerprint, prevent_rename, - is_partial_download.is_some(), + is_partial_download.is_some() + && mime_parser + .get_header(HeaderDef::ContentType) + .unwrap_or_default() + .starts_with("multipart/encrypted"), ) .await? { From a6b5645dfb7f70a3fb35086e9e645ae8e19ec12b Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 8 Jun 2025 15:25:50 +0000 Subject: [PATCH 354/381] make lookup_pgp_contact_by_fingerprint safer --- src/receive_imf.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index ec61577fc1..dcef610b36 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -3646,11 +3646,16 @@ async fn lookup_pgp_contact_by_fingerprint( context: &Context, fingerprint: &str, ) -> Result> { + debug_assert!(!fingerprint.is_empty()); + if fingerprint.is_empty() { + // Avoid accidentally looking up a non-PGP contact. + return Ok(None); + } if let Some(contact_id) = context .sql .query_row_optional( "SELECT id FROM contacts - WHERE contacts.fingerprint=?", + WHERE fingerprint=? AND fingerprint!=''", (fingerprint,), |row| { let contact_id: ContactId = row.get(0)?; From ce995bc3f051bed86e86c9fb650414429857d691 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 8 Jun 2025 15:30:08 +0000 Subject: [PATCH 355/381] remove TODO --- src/mimeparser.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 613b848c91..8fc35b589b 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -1924,7 +1924,6 @@ async fn parse_gossip_headers( } let fingerprint = header.public_key.dc_fingerprint().hex(); - // TODO: header.public_key.verify() ? context .sql .execute( From a594978e2755e8ee923ba5751cb810bdba1c3a13 Mon Sep 17 00:00:00 2001 From: l Date: Sun, 8 Jun 2025 16:47:53 +0000 Subject: [PATCH 356/381] Update src/mimefactory.rs Co-authored-by: Hocuri --- src/mimefactory.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 8b895d969a..e56edce89d 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -213,7 +213,7 @@ impl MimeFactory { encryption_keys = if msg .param .get_bool(Param::ForcePlaintext) - .unwrap_or_default() + .unwrap_or(false) { None } else { From 0658fd81d84e30647286aac4097d823bfb7d7f5e Mon Sep 17 00:00:00 2001 From: l Date: Sun, 8 Jun 2025 16:53:51 +0000 Subject: [PATCH 357/381] Update src/securejoin.rs Co-authored-by: Hocuri --- src/securejoin.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/securejoin.rs b/src/securejoin.rs index 6b7d50b320..c1f81c2289 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -307,15 +307,12 @@ pub(crate) async fn handle_securejoin_handshake( inviter_progress(context, contact_id, 300); let from_addr = ContactAddress::new(&mime_message.from.addr)?; - let autocrypt_fingerprint = mime_message - .autocrypt_fingerprint - .clone() - .unwrap_or_default(); + let autocrypt_fingerprint = mime_message.autocrypt_fingerprint.as_deref().unwrap_or(""); let (autocrypt_contact_id, _) = Contact::add_or_lookup_ex( context, "", &from_addr, - &autocrypt_fingerprint, + autocrypt_fingerprint, Origin::IncomingUnknownFrom, ) .await?; From 1602b6681e6e60153df1dbe630f821ac02f4c3e1 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 8 Jun 2025 16:55:43 +0000 Subject: [PATCH 358/381] rustfmt --- src/mimefactory.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index e56edce89d..f34368e6d1 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -210,11 +210,7 @@ impl MimeFactory { if chat.is_self_talk() { to.push((from_displayname.to_string(), from_addr.to_string())); - encryption_keys = if msg - .param - .get_bool(Param::ForcePlaintext) - .unwrap_or(false) - { + encryption_keys = if msg.param.get_bool(Param::ForcePlaintext).unwrap_or(false) { None } else { // Encrypt, but only to self. From 24edee8efd9e5f3caeddf19a7b1b85184e2cb64e Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 8 Jun 2025 18:16:33 +0000 Subject: [PATCH 359/381] check if the key is available in encryption info --- src/contact.rs | 6 +++++- src/contact/contact_tests.rs | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/contact.rs b/src/contact.rs index 08e0aeea2a..c29963578b 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -1265,7 +1265,11 @@ impl Contact { }; let fingerprint_other = fingerprint_other.to_string(); - let stock_message = stock_str::e2e_available(context).await; + let stock_message = if contact.public_key(context).await?.is_some() { + stock_str::e2e_available(context).await + } else { + stock_str::encr_none(context).await + }; let finger_prints = stock_str::finger_prints(context).await; let mut ret = format!("{stock_message}.\n{finger_prints}:"); diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs index 86b412bec2..dece9fd425 100644 --- a/src/contact/contact_tests.rs +++ b/src/contact/contact_tests.rs @@ -771,6 +771,25 @@ CCCB 5AA9 F6E1 141C 9431 ); let contact = Contact::get_by_id(alice, contact_bob_id).await?; assert!(contact.e2ee_avail(alice).await?); + + alice.sql.execute("DELETE FROM public_keys", ()).await?; + let encrinfo = Contact::get_encrinfo(alice, contact_bob_id).await?; + assert_eq!( + encrinfo, + "No encryption. +Fingerprints: + +Me (alice@example.org): +2E6F A2CB 23B5 32D7 2863 +4B58 64B0 8F61 A9ED 9443 + +bob@example.net (bob@example.net): +CCCB 5AA9 F6E1 141C 9431 +65F1 DB18 B18C BCF7 0487" + ); + let contact = Contact::get_by_id(alice, contact_bob_id).await?; + assert!(!contact.e2ee_avail(alice).await?); + Ok(()) } From cbd46622bb7df55f58922c95b9efb2032fbaa2c0 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 8 Jun 2025 20:05:10 +0000 Subject: [PATCH 360/381] add key missing note to chat encryption info --- src/chat.rs | 6 +++++- src/chat/chat_tests.rs | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/chat.rs b/src/chat.rs index b48991c389..9a7bb24aef 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1340,7 +1340,11 @@ impl ChatId { let fingerprint = contact .fingerprint() .context("Contact does not have a fingerprint in encrypted chat")?; - ret += &format!("\n{addr}\n{fingerprint}\n"); + if contact.public_key(context).await?.is_some() { + ret += &format!("\n{addr}\n{fingerprint}\n"); + } else { + ret += &format!("\n{addr}\n(key missing)\n{fingerprint}\n"); + } } Ok(ret.trim().to_string()) diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 355780d7c9..3dbec2c8ed 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -2765,6 +2765,22 @@ async fn test_chat_get_encryption_info() -> Result<()> { "No encryption" ); + alice.sql.execute("DELETE FROM public_keys", ()).await?; + assert_eq!( + chat_id.get_encryption_info(alice).await?, + "End-to-end encryption available\n\ + \n\ + fiona@example.net\n\ + (key missing)\n\ + C8BA 50BF 4AC1 2FAF 38D7\n\ + F657 DDFC 8E9F 3C79 9195\n\ + \n\ + bob@example.net\n\ + (key missing)\n\ + CCCB 5AA9 F6E1 141C 9431\n\ + 65F1 DB18 B18C BCF7 0487" + ); + Ok(()) } From 7f9c6b993c2397bedbf148f3d832fe84ebe344b7 Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 9 Jun 2025 14:33:51 +0000 Subject: [PATCH 361/381] comment for None to_id --- src/receive_imf.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index dcef610b36..7080073152 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -493,6 +493,17 @@ pub(crate) async fn receive_imf_inner( ) .await?; + // `None` means that the chat is encrypted, + // but we were not able to convert the address + // to PGP-contact, e.g. + // because there wase no corresponding + // Autocrypt-Gossip header. + // + // This way we still preserve remaining + // contact number and positions + // so we can match them contacts to + // e.g. Chat-Group-Member-Timestamps + // header. let to_ids: Vec>; let past_ids: Vec>; From e0607b37482611f0999e88ee82b4631bca8d392e Mon Sep 17 00:00:00 2001 From: Hocuri Date: Tue, 10 Jun 2025 16:43:08 +0200 Subject: [PATCH 362/381] feat: If any group member is in RESET peerstate, downgrade to Ad-Hoc group (#6905) I agree with holger that it's most important to allow communication in mixed/ambiguous groups, even if you can't modify the member list anymore. So, if any member of a group is in RESET peerstate, this downgrades the whole group to an Ad-Hoc group, rather than removing this member. Except for chatmail accounts, where we ignore the Reset state already before PGP-contacts, so, it's ignored in the migration, too. Broadcast lists keep the current behavior. Fix #6908 --- src/sql/migrations.rs | 91 +++++++++++++++++++++++++++++-------------- 1 file changed, 62 insertions(+), 29 deletions(-) diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 5d136ed3cb..42813477c9 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1559,6 +1559,7 @@ fn migrate_pgp_contacts( .context("Step 20")? .collect::, rusqlite::Error>>() .context("Step 21")?; + { let mut stmt = transaction .prepare( @@ -1578,6 +1579,37 @@ fn migrate_pgp_contacts( .context("Step 23")?; let mut load_chat_contacts_stmt = transaction .prepare("SELECT contact_id FROM chats_contacts WHERE chat_id=? AND contact_id>9")?; + let is_chatmail: Option = transaction + .query_row( + "SELECT value FROM config WHERE keyname='is_chatmail'", + (), + |row| row.get(0), + ) + .optional() + .context("Step 23.1")?; + let is_chatmail = is_chatmail + .and_then(|s| s.parse::().ok()) + .unwrap_or_default() + != 0; + let map_to_pgp_contact = |old_member: &u32| { + ( + *old_member, + autocrypt_pgp_contacts + .get(old_member) + .or_else(|| { + // For chatmail servers, + // we send encrypted even if the peerstate is reset, + // because an unencrypted message likely won't arrive. + // This is the same behavior as before PGP-contacts migration. + if is_chatmail { + autocrypt_pgp_contacts_with_reset_peerstate.get(old_member) + } else { + None + } + }) + .copied(), + ) + }; let mut update_member_stmt = transaction .prepare("UPDATE chats_contacts SET contact_id=? WHERE contact_id=? AND chat_id=?")?; @@ -1598,29 +1630,6 @@ fn migrate_pgp_contacts( orphaned_contacts.remove(m); } }; - let retain_autocrypt_pgp_contacts = || { - old_members - .iter() - .map(|original| { - ( - *original, - autocrypt_pgp_contacts - .get(original) - // TODO it's unclear whether we want to do this: - // We could also make the group unencrypted - // if any peerstate is reset. - // Also, right now, if we have no key at all, - // the member will be silently removed from the group; - // maybe we should at least post an info message? - .or_else(|| { - autocrypt_pgp_contacts_with_reset_peerstate.get(original) - }) - .copied(), - ) - }) - .collect::)>>() - }; - let old_and_new_members: Vec<(u32, Option)> = match typ { // 1:1 chats retain: // - email-contact if peerstate is in the "reset" state, @@ -1634,7 +1643,8 @@ fn migrate_pgp_contacts( info!(context, "1:1 chat {chat_id} doesn't contain contact, probably it's self or device chat"); continue; }; - let Some(&new_contact) = autocrypt_pgp_contacts.get(old_member) else { + + let (_, Some(new_contact)) = map_to_pgp_contact(old_member) else { keep_email_contacts("No peerstate, or peerstate in 'reset' state"); continue; }; @@ -1668,7 +1678,10 @@ fn migrate_pgp_contacts( }) .collect() } else { - retain_autocrypt_pgp_contacts() + old_members + .iter() + .map(map_to_pgp_contact) + .collect::)>>() } } @@ -1679,16 +1692,36 @@ fn migrate_pgp_contacts( } // Broadcast list - 160 => retain_autocrypt_pgp_contacts(), - + 160 => old_members + .iter() + .map(|original| { + ( + *original, + autocrypt_pgp_contacts + .get(original) + // There will be no unencrypted broadcast lists anymore, + // so, if a peerstate is reset, + // the best we can do is encrypting to this key regardless. + .or_else(|| { + autocrypt_pgp_contacts_with_reset_peerstate.get(original) + }) + .copied(), + ) + }) + .collect::)>>(), _ => { warn!(context, "Invalid chat type {typ}"); continue; } }; - if old_and_new_members.iter().all(|(_old, new)| new.is_none()) { - keep_email_contacts("All contacts in chat are e-mail contacts"); + // If a group contains a contact without a key or with 'reset' peerstate, + // downgrade to unencrypted Ad-Hoc group. + if typ == 120 && old_and_new_members.iter().any(|(_old, new)| new.is_none()) { + transaction + .execute("UPDATE chats SET grpid='' WHERE id=?", (chat_id,)) + .context("Step 26.1")?; + keep_email_contacts("Group contains contact without peerstate"); continue; } From 4eb1a5eb5048ba91cb18676a36d9ba89ad6e1613 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Fri, 20 Jun 2025 19:27:56 +0200 Subject: [PATCH 363/381] feat: Show an email-avatar for email-contacts and email-chats (#6916) Mark email-contacts and email-chats as such by setting a special avatar. Then, the UIs can remove the letter-icon from next to the name again, and we have less overall clutter in Delta Chat. ![Screenshot_20250613-160033](https://github.com/user-attachments/assets/eca9e896-ba1e-4dab-9819-78083a07a18f) --- assets/icon-email-contact.png | Bin 0 -> 3491 bytes assets/icon-email-contact.svg | 47 ++++++ src/chat.rs | 143 +++++++++--------- src/chat/chat_tests.rs | 51 ++++--- src/contact.rs | 21 ++- src/sql.rs | 10 +- src/test_utils.rs | 2 +- .../receive_imf_older_message_from_2nd_device | 2 +- test-data/golden/test_old_message_5 | 2 +- test-data/golden/test_outgoing_mua_msg | 2 +- 10 files changed, 176 insertions(+), 104 deletions(-) create mode 100644 assets/icon-email-contact.png create mode 100644 assets/icon-email-contact.svg diff --git a/assets/icon-email-contact.png b/assets/icon-email-contact.png new file mode 100644 index 0000000000000000000000000000000000000000..ecd289e928d630d986899f109e745b3a8382eaef GIT binary patch literal 3491 zcmeHKYc$*07XK^tD)oLmA zlBhwY9_h5IN`%{b1o0fCMZ7{#+F4g`|R^u=d9m4*&c2V zlH$k20RWJ6ylCqQ00L8AO%x0OJ{{AJ;BX@NqNghW;I#nYW(EN4f}xvi0EjaLfX_$( zFuxA~N>QcMOHeRzD9G8t7T^=}dpp3HSky%X4gik6{%QhU*y<24C=%=FYA5nZSVCIM zMB7>28UQ3*9c?eb6GoR=`#AWZTGtvD_G7`L48=5Yr;8`V!|k>6X10N!BwxEMzm7%g z7~kRY*Yc~4(mzzGe^gO>?%{Dpo|0C7UQ9Mk(gUI^Er-7e8sP(U$!x;I* zvd8D)sbS=+Tj~AAE+bO|>HDOx7p}IcVXgfSIFY=s%?L0>cajEsb3@H zSNz;hyuM7x%$GozRMdx=NzdH}%=8Y#F?>czkl5Zw19eeLSYc(yE}SiM{g!4F&fzj! z=K1Bp0eO7J;*2Pipz(~17<_wfB4$Rkv_uAvf$)Xjls>Iyl=6vRIHrPYBxSGpoH$!w^T;v-nnXtHt+g3pqrcUvmrg~$nlzAOfQ z-5Z3ZeI8`EAF)gic#}$hV6tMSJPffT9&S<3giQNCK8iLT(xdZDxzfC{)t!6M2BBSQ zBj{;WkuAH#Hy>LMUiE9p%Mg%H5e>g?Sd)mW)r5!}Y?+)KD+;sKHGp$W%rc{AJ+6`D zpsk-uyD~@(Ct%qpIi3#*P_Agj#gi1~Qa~J*AHY5-4k!En{5D@d?_2s+$RtYvpEU*@ zmD#Kxz<7$G8(U6|kuxAOS)UmDnw;SBu{RV*VwDZ&2tg6+uh5m>xD$`*yU?d5Zdq(| zn>=;I;^B=~jRodOten18r8s&bGYNebkDScuZ!mkKJo47cdSH6*)+psuLtA-Zm~Z<~ z+Y*6l+~;-ulCa4l|K~3!8mP6W_;Iur)WSG{ODwmHfJ7i0G^)m(*_&iFrkQ+rmYp`b(E*VYOh%W?RGC9cpoXU8tvZ z<4`+$&o5~9Ze9Ag>atg|U-f{KYz z%n55YHCI=64&?o?mx@8B#1nDDGvkg3?JbDvFP~hdcXNdfmG~W$6DFfCM+SFV&-5-P zjO=W`o0PJ}Un}>JO{G)uLk7{72OgP9Igz7faRy!7`C!E8nJo$MQa^sWa>I%Zk0m9L zzG&049$zhOuYw;ciA!0wfEEQD?NXPpY&kgD%f=B$|gFIv7?A?hu% zhIvOdagKQiPmr90i6m;&ZmxSsy9cyqzY<&c(JngsaMNAZX_LMFO%a5Fytn0Y%Rg3Q zf{Gfo6i1eN$2cD#OnrIAa>ARPZJ*VB;ZSEiO5XAca|&uizfO8lSxZl9naadW$4lJX z4*{EP&-H!izX)wE7=W$)Ub^8&v*%StgJ(=j#k4LK5Efh%k91D-9RBi6N8f-(Ck?fq zvQ@fue-&0Arj#w2@>B1VJXi#HGpzQVau?UK_2e1&PMprYTc&;rcdsE;w`n*GM@TUz zso66d|KYMsx9}F0S%YZcwhv7&%tLvFw+UcWOr&Xg?-EaBqJ^A-*!rZkQVZYRsDfD! zWM4F+ASM-gg|#(9m1+U^HdC(lPg|Mfq=29Z5wO&Q&5fAkdVGGKzS}?R9=&&jQkXln zWzO|IKH6I>r&HOk-gcVmBORenO?V*(o?`rTbDlsEvl!!8HmY5ajN}*U5ti~~|J|Qs zC|ERuhVnZ-?p>o0puIKWG#7G~CwE`j9;uoz*}#7s_<_{1V=YX`&yyXEy)2|jN$I(= zS);WAtLI%gWE0k98WT8 ziD+9G(~uhjYkW(|Te6xysPJuG%eE*?Pi*#d#DZ;9(@85bgtNc5SMF(->5q|Dh6t5v0%9@=IUqoOiHOJ)4COBi@cn&QK0-FRQan;Y?)r&LfX&= zF&*fH%4>)4+*x=PO`iX) z-)p=%GQFtJ|JLGckS!T$+^~jQfaL>nbfWGPiNjf5$a86B`m38)_aL6ABK1k)hG|dWI%?-x7}h439VI3}RHiV!W`Z*!aMhP#``&UO(b`BrZ5G ZDpVgE6IQ~39s?Hvj&^RgbueVwzW{64IFSGV literal 0 HcmV?d00001 diff --git a/assets/icon-email-contact.svg b/assets/icon-email-contact.svg new file mode 100644 index 0000000000..0cb9d97a89 --- /dev/null +++ b/assets/icon-email-contact.svg @@ -0,0 +1,47 @@ + + + + + + + diff --git a/src/chat.rs b/src/chat.rs index 9a7bb24aef..646c4d7eeb 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1730,25 +1730,36 @@ impl Chat { /// Returns profile image path for the chat. pub async fn get_profile_image(&self, context: &Context) -> Result> { - if let Some(image_rel) = self.param.get(Param::ProfileImage) { - if !image_rel.is_empty() { - return Ok(Some(get_abs_path(context, Path::new(&image_rel)))); - } - } else if self.id.is_archived_link() { - if let Ok(image_rel) = get_archive_icon(context).await { - return Ok(Some(get_abs_path(context, Path::new(&image_rel)))); - } + if self.id.is_archived_link() { + // This is not a real chat, but the "Archive" button + // that is shown at the top of the chats list + return Ok(Some(get_archive_icon(context).await?)); + } else if self.is_device_talk() { + return Ok(Some(get_device_icon(context).await?)); + } else if self.is_self_talk() { + return Ok(Some(get_saved_messages_icon(context).await?)); } else if self.typ == Chattype::Single { + // For 1:1 chats, we always use the same avatar as for the contact + // This is before the `self.is_encrypted()` check, because that function + // has two database calls, i.e. it's slow let contacts = get_chat_contacts(context, self.id).await?; if let Some(contact_id) = contacts.first() { - if let Ok(contact) = Contact::get_by_id(context, *contact_id).await { - return contact.get_profile_image(context).await; - } + let contact = Contact::get_by_id(context, *contact_id).await?; + return contact.get_profile_image(context).await; } - } else if self.typ == Chattype::Broadcast { - if let Ok(image_rel) = get_broadcast_icon(context).await { + } else if !self.is_encrypted(context).await? { + // This is an email-contact chat, show a special avatar that marks it as such + return Ok(Some(get_abs_path( + context, + Path::new(&get_email_contact_icon(context).await?), + ))); + } else if let Some(image_rel) = self.param.get(Param::ProfileImage) { + // Load the group avatar, or the device-chat / saved-messages icon + if !image_rel.is_empty() { return Ok(Some(get_abs_path(context, Path::new(&image_rel)))); } + } else if self.typ == Chattype::Broadcast { + return Ok(Some(get_broadcast_icon(context).await?)); } Ok(None) } @@ -2422,69 +2433,63 @@ pub struct ChatInfo { // - [ ] email } -pub(crate) async fn update_saved_messages_icon(context: &Context) -> Result<()> { - if let Some(ChatIdBlocked { id: chat_id, .. }) = - ChatIdBlocked::lookup_by_contact(context, ContactId::SELF).await? - { - let icon = include_bytes!("../assets/icon-saved-messages.png"); - let blob = - BlobObject::create_and_deduplicate_from_bytes(context, icon, "saved-messages.png")?; - let icon = blob.as_name().to_string(); - - let mut chat = Chat::load_from_db(context, chat_id).await?; - chat.param.set(Param::ProfileImage, icon); - chat.update_param(context).await?; +async fn get_asset_icon(context: &Context, name: &str, bytes: &[u8]) -> Result { + ensure!(name.starts_with("icon-")); + if let Some(icon) = context.sql.get_raw_config(name).await? { + return Ok(get_abs_path(context, Path::new(&icon))); } - Ok(()) -} -pub(crate) async fn update_device_icon(context: &Context) -> Result<()> { - if let Some(ChatIdBlocked { id: chat_id, .. }) = - ChatIdBlocked::lookup_by_contact(context, ContactId::DEVICE).await? - { - let icon = include_bytes!("../assets/icon-device.png"); - let blob = BlobObject::create_and_deduplicate_from_bytes(context, icon, "device.png")?; - let icon = blob.as_name().to_string(); + let blob = + BlobObject::create_and_deduplicate_from_bytes(context, bytes, &format!("{name}.png"))?; + let icon = blob.as_name().to_string(); + context.sql.set_raw_config(name, Some(&icon)).await?; - let mut chat = Chat::load_from_db(context, chat_id).await?; - chat.param.set(Param::ProfileImage, &icon); - chat.update_param(context).await?; + Ok(get_abs_path(context, Path::new(&icon))) +} - let mut contact = Contact::get_by_id(context, ContactId::DEVICE).await?; - contact.param.set(Param::ProfileImage, icon); - contact.update_param(context).await?; - } - Ok(()) +pub(crate) async fn get_saved_messages_icon(context: &Context) -> Result { + get_asset_icon( + context, + "icon-saved-messages", + include_bytes!("../assets/icon-saved-messages.png"), + ) + .await } -pub(crate) async fn get_broadcast_icon(context: &Context) -> Result { - if let Some(icon) = context.sql.get_raw_config("icon-broadcast").await? { - return Ok(icon); - } +pub(crate) async fn get_device_icon(context: &Context) -> Result { + get_asset_icon( + context, + "icon-device", + include_bytes!("../assets/icon-device.png"), + ) + .await +} - let icon = include_bytes!("../assets/icon-broadcast.png"); - let blob = BlobObject::create_and_deduplicate_from_bytes(context, icon, "broadcast.png")?; - let icon = blob.as_name().to_string(); - context - .sql - .set_raw_config("icon-broadcast", Some(&icon)) - .await?; - Ok(icon) +pub(crate) async fn get_broadcast_icon(context: &Context) -> Result { + get_asset_icon( + context, + "icon-broadcast", + include_bytes!("../assets/icon-broadcast.png"), + ) + .await } -pub(crate) async fn get_archive_icon(context: &Context) -> Result { - if let Some(icon) = context.sql.get_raw_config("icon-archive").await? { - return Ok(icon); - } +pub(crate) async fn get_archive_icon(context: &Context) -> Result { + get_asset_icon( + context, + "icon-archive", + include_bytes!("../assets/icon-archive.png"), + ) + .await +} - let icon = include_bytes!("../assets/icon-archive.png"); - let blob = BlobObject::create_and_deduplicate_from_bytes(context, icon, "archive.png")?; - let icon = blob.as_name().to_string(); - context - .sql - .set_raw_config("icon-archive", Some(&icon)) - .await?; - Ok(icon) +pub(crate) async fn get_email_contact_icon(context: &Context) -> Result { + get_asset_icon( + context, + "icon-email-contact", + include_bytes!("../assets/icon-email-contact.png"), + ) + .await } async fn update_special_chat_name( @@ -2658,12 +2663,6 @@ impl ChatIdBlocked { .await?; } - match contact_id { - ContactId::SELF => update_saved_messages_icon(context).await?, - ContactId::DEVICE => update_device_icon(context).await?, - _ => (), - } - Ok(Self { id: chat_id, blocked: create_blocked, diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 3dbec2c8ed..10c05cf969 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -7,6 +7,7 @@ use crate::imex::{has_backup, imex, ImexMode}; use crate::message::{delete_msgs, MessengerMessage}; use crate::receive_imf::receive_imf; use crate::test_utils::{sync, TestContext, TestContextManager, TimeShiftFalsePositiveNote}; +use pretty_assertions::assert_eq; use strum::IntoEnumIterator; use tokio::fs; @@ -19,26 +20,34 @@ async fn test_chat_info() { // Ensure we can serialize this. println!("{}", serde_json::to_string_pretty(&info).unwrap()); - let expected = r#" - { - "id": 10, - "type": 100, - "name": "bob", - "archived": false, - "param": "", - "gossiped_timestamp": 0, - "is_sending_locations": false, - "color": 35391, - "profile_image": "", - "draft": "", - "is_muted": false, - "ephemeral_timer": "Disabled" - } - "#; + let expected = format!( + r#"{{ + "id": 10, + "type": 100, + "name": "bob", + "archived": false, + "param": "", + "is_sending_locations": false, + "color": 35391, + "profile_image": {}, + "draft": "", + "is_muted": false, + "ephemeral_timer": "Disabled" +}}"#, + // We need to do it like this so that the test passes on Windows: + serde_json::to_string( + t.get_blobdir() + .join("9a17b32ad5ff71df91f7cfda9a62bb2.png") + .to_str() + .unwrap() + ) + .unwrap() + ); // Ensure we can deserialize this. - let loaded: ChatInfo = serde_json::from_str(expected).unwrap(); - assert_eq!(info, loaded); + serde_json::from_str::(&expected).unwrap(); + + assert_eq!(serde_json::to_string_pretty(&info).unwrap(), expected); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -907,7 +916,11 @@ async fn test_add_device_msg_labelled() -> Result<()> { assert!(chat.why_cant_send(&t).await? == Some(CantSendReason::DeviceChat)); assert_eq!(chat.name, stock_str::device_messages(&t).await); - assert!(chat.get_profile_image(&t).await?.is_some()); + let device_msg_icon = chat.get_profile_image(&t).await?.unwrap(); + assert_eq!( + device_msg_icon.metadata()?.len(), + include_bytes!("../../assets/icon-device.png").len() as u64 + ); // delete device message, make sure it is not added again message::delete_msgs(&t, &[*msg1_id.as_ref().unwrap()]).await?; diff --git a/src/contact.rs b/src/contact.rs index c29963578b..ab22e00cd3 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -268,7 +268,7 @@ pub async fn make_vcard(context: &Context, contacts: &[ContactId]) -> Result None, Some(path) => tokio::fs::read(path) .await @@ -1493,11 +1493,28 @@ impl Contact { /// This is the image set by each remote user on their own /// using set_config(context, "selfavatar", image). pub async fn get_profile_image(&self, context: &Context) -> Result> { + self.get_profile_image_ex(context, true).await + } + + /// Get the contact's profile image. + /// This is the image set by each remote user on their own + /// using set_config(context, "selfavatar", image). + async fn get_profile_image_ex( + &self, + context: &Context, + show_fallback_icon: bool, + ) -> Result> { if self.id == ContactId::SELF { if let Some(p) = context.get_config(Config::Selfavatar).await? { return Ok(Some(PathBuf::from(p))); // get_config() calls get_abs_path() internally already } - } else if let Some(image_rel) = self.param.get(Param::ProfileImage) { + } else if self.id == ContactId::DEVICE { + return Ok(Some(chat::get_device_icon(context).await?)); + } + if show_fallback_icon && !self.id.is_special() && !self.is_pgp_contact() { + return Ok(Some(chat::get_email_contact_icon(context).await?)); + } + if let Some(image_rel) = self.param.get(Param::ProfileImage) { if !image_rel.is_empty() { return Ok(Some(get_abs_path(context, Path::new(image_rel)))); } diff --git a/src/sql.rs b/src/sql.rs index 93f37e0a90..f4d7f6a386 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -8,7 +8,7 @@ use rusqlite::{config::DbConfig, types::ValueRef, Connection, OpenFlags, Row}; use tokio::sync::RwLock; use crate::blob::BlobObject; -use crate::chat::{add_device_msg, update_device_icon, update_saved_messages_icon}; +use crate::chat::add_device_msg; use crate::config::Config; use crate::constants::DC_CHAT_ID_TRASH; use crate::context::Context; @@ -213,18 +213,14 @@ impl Sql { // this should be done before updates that use high-level objects that // rely themselves on the low-level structure. - let (update_icons, disable_server_delete, recode_avatar) = migrations::run(context, self) + // `update_icons` is not used anymore, since it's not necessary anymore to "update" icons: + let (_update_icons, disable_server_delete, recode_avatar) = migrations::run(context, self) .await .context("failed to run migrations")?; // (2) updates that require high-level objects // the structure is complete now and all objects are usable - if update_icons { - update_saved_messages_icon(context).await?; - update_device_icon(context).await?; - } - if disable_server_delete { // We now always watch all folders and delete messages there if delete_server is enabled. // So, for people who have delete_server enabled, disable it and add a hint to the devicechat: diff --git a/src/test_utils.rs b/src/test_utils.rs index 608a80d196..291189ba0d 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -976,7 +976,7 @@ impl TestContext { "" }, match sel_chat.get_profile_image(self).await.unwrap() { - Some(icon) => match icon.to_str() { + Some(icon) => match icon.strip_prefix(self.get_blobdir()).unwrap().to_str() { Some(icon) => format!(" Icon: {icon}"), _ => " Icon: Err".to_string(), }, diff --git a/test-data/golden/receive_imf_older_message_from_2nd_device b/test-data/golden/receive_imf_older_message_from_2nd_device index 13fe53ba44..993c99f9f5 100644 --- a/test-data/golden/receive_imf_older_message_from_2nd_device +++ b/test-data/golden/receive_imf_older_message_from_2nd_device @@ -1,4 +1,4 @@ -Single#Chat#10: bob@example.net [bob@example.net] +Single#Chat#10: bob@example.net [bob@example.net] Icon: 9a17b32ad5ff71df91f7cfda9a62bb2.png -------------------------------------------------------------------------------- Msg#10: Me (Contact#Contact#Self): We share this account √ Msg#11: Me (Contact#Contact#Self): I'm Alice too √ diff --git a/test-data/golden/test_old_message_5 b/test-data/golden/test_old_message_5 index 75d3c0af0d..473f0cdc49 100644 --- a/test-data/golden/test_old_message_5 +++ b/test-data/golden/test_old_message_5 @@ -1,4 +1,4 @@ -Single#Chat#10: Bob [bob@example.net] +Single#Chat#10: Bob [bob@example.net] Icon: 9a17b32ad5ff71df91f7cfda9a62bb2.png -------------------------------------------------------------------------------- Msg#10: Me (Contact#Contact#Self): Happy birthday, Bob! √ Msg#11: (Contact#Contact#10): Happy birthday to me, Alice! [FRESH] diff --git a/test-data/golden/test_outgoing_mua_msg b/test-data/golden/test_outgoing_mua_msg index abab166cbe..a96d9f9b30 100644 --- a/test-data/golden/test_outgoing_mua_msg +++ b/test-data/golden/test_outgoing_mua_msg @@ -1,4 +1,4 @@ -Single#Chat#11: bob@example.net [bob@example.net] +Single#Chat#11: bob@example.net [bob@example.net] Icon: 9a17b32ad5ff71df91f7cfda9a62bb2.png -------------------------------------------------------------------------------- Msg#12: Me (Contact#Contact#Self): One classical MUA message √ -------------------------------------------------------------------------------- From 9909fb862a76cc20ebc145db7c22c53e929b5551 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Sat, 21 Jun 2025 18:56:46 +0200 Subject: [PATCH 364/381] doc: Adapt the comment on `MessageObject.showPadlock` (#6925) We want to remove the padlock-icon from messages, and instead show an icon if the message was unencrypted: https://github.com/deltachat/deltachat-ios/pull/2746/ Now, it's confusing for implementers that there is a field `showPadlock` in the jsonrpc. This PR adapts the comment. Two alternatives to this PR would be: - Deprecate `showPadlock`, and introduce a field `wasEncrypted` - Deprecate `showPadlock`, and introduce a field `showLetterIcon`, which is the inverse of `showPadlock`. --- deltachat-jsonrpc/src/api/types/message.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/deltachat-jsonrpc/src/api/types/message.rs b/deltachat-jsonrpc/src/api/types/message.rs index 2b9858d3e8..3c1d7ab80c 100644 --- a/deltachat-jsonrpc/src/api/types/message.rs +++ b/deltachat-jsonrpc/src/api/types/message.rs @@ -61,6 +61,13 @@ pub struct MessageObject { // summary - use/create another function if you need it subject: String, + + /// True if the message was correctly encrypted&signed, false otherwise. + /// Historically, UIs showed a small padlock on the message then. + /// + /// Today, the UIs should instead show a small email-icon on the message + /// if `show_padlock` is `false`, + /// and nothing if it is `true`. show_padlock: bool, is_setupmessage: bool, is_info: bool, From 54ca1c55aaca1dec324c0b1af2472fb13af94b9a Mon Sep 17 00:00:00 2001 From: Hocuri Date: Sat, 21 Jun 2025 21:44:00 +0200 Subject: [PATCH 365/381] feat: increase DCBACKUP_VERSION with PGP-contacts (#6929) This way, we prevent users from running into problems when importing an old backup. Note that this is not strictly necessary, but a decision of assuming that this is better for usability - importing a new backup into an old DC version would mostly work fine, but there would be weird bugs. E.g., after updating the imported profile to PGP-contacts, the migration wouldn't run a second time, so that all peerstate changes since the device was updated would get lost. Desktop|Android -|- ![image](https://github.com/user-attachments/assets/b9fc6f3a-f778-4e64-ad2c-393d5c756ded) | ![image](https://github.com/user-attachments/assets/65aceb7e-eeba-45e3-8f00-0c676a218f9d) Not sure why Desktop shows a nice error message, while Android shows a cryptic "IMEX failed to complete", though for this release, it's too late to change this (except if we want to make a quick release before PGP-contacts, which improves the error message). _Edit:_ It's because I used Add-second-device for Desktop, and import-file for Android. Would be nice to improve the error message when importing a backup, but this is out of scope for this PR. I created an issue: https://github.com/chatmail/core/issues/6930 This is not a blocker for the PGP-contacts release, because as I said, it's too late to change this except if we want to make another release before the PGP-contacts release. --- src/qr.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qr.rs b/src/qr.rs index 87167f58ff..0afe6c6677 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -41,7 +41,7 @@ pub(crate) const DCBACKUP_SCHEME_PREFIX: &str = "DCBACKUP"; /// Version written to Backups and Backup-QR-Codes. /// Imports will fail when they have a larger version. -pub(crate) const DCBACKUP_VERSION: i32 = 2; +pub(crate) const DCBACKUP_VERSION: i32 = 3; /// Scanned QR code. #[derive(Debug, Clone, PartialEq, Eq)] From 4f160a6409f9e4bf8c722fc3d830f86d17069184 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Sat, 21 Jun 2025 21:44:22 +0200 Subject: [PATCH 366/381] feat: get_showpadlock() returns true for device messages (#6931) Follow-up to https://github.com/chatmail/core/pull/6925, reasoning is in this comment: https://github.com/chatmail/core/pull/6925#issuecomment-2988282425 --- src/message.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/message.rs b/src/message.rs index 0f1e9da1f0..91351dd6ff 100644 --- a/src/message.rs +++ b/src/message.rs @@ -835,6 +835,7 @@ impl Message { /// Returns true if padlock indicating message encryption should be displayed in the UI. pub fn get_showpadlock(&self) -> bool { self.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() != 0 + || self.from_id == ContactId::DEVICE } /// Returns true if message is auto-generated. From c0cf4e7e9b70c9d2201a1e30e5a0e782859ab95b Mon Sep 17 00:00:00 2001 From: Hocuri Date: Mon, 23 Jun 2025 14:03:54 +0200 Subject: [PATCH 367/381] fix: Also migrate messages in PGP-contacts migration (#6921) Previously, messages were not rewritten. This meant that all messages stayed with the old email-identified contact. #6916 made it very obvious that all messages sent into a group before the PGP-contacts migration got the email avatar. With this PR, all encrypted messages are rewritten to the PGP-contact identified by the current autocrypt key. It is not possible to find out which key was actually used to sign the message. --------- Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com> --- src/sql/migrations.rs | 90 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 77 insertions(+), 13 deletions(-) diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 36a7122587..cb1463706f 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1240,7 +1240,11 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint); let start = Instant::now(); sql.execute_migration_transaction(|t| migrate_pgp_contacts(context, t), migration_version) .await?; - info!(context, "PGP contacts migration took {:?}", start.elapsed()); + info!( + context, + "PGP contacts migration took {:?} in total.", + start.elapsed() + ); } let new_version = sql @@ -1322,7 +1326,7 @@ fn migrate_pgp_contacts( // Create up to 3 new contacts for every contact that has a peerstate: // one from the Autocrypt key fingerprint, one from the verified key fingerprint, // one from the secondary verified key fingerprint. - // In the process, build two maps from old contact id to new contact id: + // In the process, build maps from old contact id to new contact id: // one that maps to Autocrypt PGP-contact, one that maps to verified PGP-contact. let mut autocrypt_pgp_contacts: BTreeMap = BTreeMap::new(); let mut autocrypt_pgp_contacts_with_reset_peerstate: BTreeMap = BTreeMap::new(); @@ -1787,17 +1791,77 @@ fn migrate_pgp_contacts( } // ======================= Step 4: ======================= - info!( - context, - "Marking contacts which remained in no chat at all as hidden: {orphaned_contacts:?}" - ); - let mut mark_as_hidden_stmt = transaction - .prepare("UPDATE contacts SET origin=? WHERE id=?") - .context("Step 30")?; - for contact in orphaned_contacts { - mark_as_hidden_stmt - .execute((0x8, contact)) - .context("Step 31")?; + { + info!( + context, + "Marking contacts which remained in no chat at all as hidden: {orphaned_contacts:?}" + ); + let mut mark_as_hidden_stmt = transaction + .prepare("UPDATE contacts SET origin=? WHERE id=?") + .context("Step 30")?; + for contact in orphaned_contacts { + mark_as_hidden_stmt + .execute((0x8, contact)) + .context("Step 31")?; + } + } + + // ======================= Step 5: ======================= + // Rewrite `from_id` in messages + { + let start = Instant::now(); + + let mut encrypted_msgs_stmt = transaction + .prepare( + "SELECT id, from_id, to_id + FROM msgs + WHERE id>9 + AND (param LIKE '%\nc=1%' OR param LIKE 'c=1%') + AND chat_id>9 + ORDER BY id DESC LIMIT 10000", + ) + .context("Step 32")?; + let mut rewrite_msg_stmt = transaction + .prepare("UPDATE msgs SET from_id=?, to_id=? WHERE id=?") + .context("Step 32.1")?; + + struct LoadedMsg { + id: u32, + from_id: u32, + to_id: u32, + } + + let encrypted_msgs = encrypted_msgs_stmt + .query_map((), |row| { + let id: u32 = row.get(0)?; + let from_id: u32 = row.get(1)?; + let to_id: u32 = row.get(2)?; + Ok(LoadedMsg { id, from_id, to_id }) + }) + .context("Step 33")?; + + for msg in encrypted_msgs { + let msg = msg.context("Step 34")?; + + let new_from_id = *autocrypt_pgp_contacts + .get(&msg.from_id) + .or_else(|| autocrypt_pgp_contacts_with_reset_peerstate.get(&msg.from_id)) + .unwrap_or(&msg.from_id); + + let new_to_id = *autocrypt_pgp_contacts + .get(&msg.to_id) + .or_else(|| autocrypt_pgp_contacts_with_reset_peerstate.get(&msg.to_id)) + .unwrap_or(&msg.to_id); + + rewrite_msg_stmt + .execute((new_from_id, new_to_id, msg.id)) + .context("Step 35")?; + } + info!( + context, + "Rewriting msgs to PGP contacts took {:?}.", + start.elapsed() + ); } Ok(()) From 7ccab834aeac5f05205bdfd080c3eb314951ff1f Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 25 Jun 2025 09:22:52 +0000 Subject: [PATCH 368/381] add TODOs for cases where group has multiple members with same address --- src/receive_imf.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 87cdc107f7..f3c3edf316 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -2804,6 +2804,14 @@ async fn apply_group_changes( } if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) { + // TODO: if address "alice@example.org" is a member of the group twice, + // with old and new key, + // and someone (maybe Alice's new contact) just removed Alice's old contact, + // we may lookup the wrong contact because we only look up by the address. + // The result is that info message may contain the new Alice's display name + // rather than old display name. + // This could be fixed by looking up the contact with the highest + // `remove_timestamp` after applying Chat-Group-Member-Timestamps. removed_id = lookup_pgp_contact_by_address(context, removed_addr, Some(chat_id)).await?; if let Some(id) = removed_id { better_msg = if id == from_id { @@ -2817,6 +2825,12 @@ async fn apply_group_changes( } } else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) { if let Some(key) = mime_parser.gossiped_keys.get(added_addr) { + // TODO: if gossiped keys contain the same address multiple times, + // we may lookup the wrong contact. + // This could be fixed by looking up the contact with + // highest `add_timestamp` to disambiguate. + // The result of the error is that info message + // may contain display name of the wrong contact. let fingerprint = key.dc_fingerprint().hex(); if let Some(contact_id) = lookup_pgp_contact_by_fingerprint(context, &fingerprint).await? From c7074b0d12cea5e8461bd082e110019947fcc645 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 25 Jun 2025 09:52:22 +0000 Subject: [PATCH 369/381] clippy fix --- src/contact.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contact.rs b/src/contact.rs index 71dd382d39..bbb33c47b8 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -1943,7 +1943,7 @@ pub(crate) async fn mark_contact_id_as_verified( } fn cat_fingerprint(ret: &mut String, name: &str, addr: &str, fingerprint: &str) { - *ret += &format!("\n\n{} ({}):\n{}", name, addr, fingerprint); + *ret += &format!("\n\n{name} ({addr}):\n{fingerprint}"); } fn split_address_book(book: &str) -> Vec<(&str, &str)> { From 4ebb23088baafcafc774520a2582f92d90faae0b Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 25 Jun 2025 10:44:14 +0000 Subject: [PATCH 370/381] extract get_to_and_past_contact_ids --- src/receive_imf.rs | 465 +++++++++++++++++++++++---------------------- 1 file changed, 243 insertions(+), 222 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index f3c3edf316..ba8c9322a0 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -214,6 +214,243 @@ async fn insert_tombstone(context: &Context, rfc724_mid: &str) -> Result Ok(msg_id) } +async fn get_to_and_past_contact_ids( + context: &Context, + mime_parser: &MimeMessage, + chat_assignment: &ChatAssignment, + is_partial_download: Option, + parent_message: &Option, + incoming_origin: Origin, +) -> Result<(Vec>, Vec>)> { + // `None` means that the chat is encrypted, + // but we were not able to convert the address + // to PGP-contact, e.g. + // because there wase no corresponding + // Autocrypt-Gossip header. + // + // This way we still preserve remaining + // contact number and positions + // so we can match them contacts to + // e.g. Chat-Group-Member-Timestamps + // header. + let to_ids: Vec>; + let past_ids: Vec>; + + // ID of the chat to look up the addresses in. + // + // Note that this is not necessarily the chat we want to assign the message to. + // In case of an outgoing private reply to a group message we may + // lookup the address of receipient in the list of addresses used in the group, + // but want to assign the message to 1:1 chat. + let chat_id = match chat_assignment { + ChatAssignment::Trash => None, + ChatAssignment::GroupChat { ref grpid } => { + if let Some((chat_id, _protected, _blocked)) = + chat::get_chat_id_by_grpid(context, grpid).await? + { + Some(chat_id) + } else { + None + } + } + ChatAssignment::AdHocGroup => { + // If we are going to assign a message to ad hoc group, + // we can just convert the email addresses + // to e-mail address contacts and don't need a `ChatId` + // to lookup PGP-contacts. + None + } + ChatAssignment::ExistingChat { chat_id, .. } => Some(*chat_id), + ChatAssignment::MailingList => None, + ChatAssignment::OneOneChat => { + if is_partial_download.is_none() && !mime_parser.incoming { + parent_message.as_ref().map(|m| m.chat_id) + } else { + None + } + } + }; + + let member_fingerprints = mime_parser.chat_group_member_fingerprints(); + let to_member_fingerprints; + let past_member_fingerprints; + + if !member_fingerprints.is_empty() { + if member_fingerprints.len() >= mime_parser.recipients.len() { + (to_member_fingerprints, past_member_fingerprints) = + member_fingerprints.split_at(mime_parser.recipients.len()); + } else { + warn!( + context, + "Unexpected length of the fingerprint header, expected at least {}, got {}.", + mime_parser.recipients.len(), + member_fingerprints.len() + ); + to_member_fingerprints = &[]; + past_member_fingerprints = &[]; + } + } else { + to_member_fingerprints = &[]; + past_member_fingerprints = &[]; + } + + let pgp_to_ids = add_or_lookup_pgp_contacts_by_address_list( + context, + &mime_parser.recipients, + &mime_parser.gossiped_keys, + to_member_fingerprints, + Origin::Hidden, + ) + .await?; + + match chat_assignment { + ChatAssignment::GroupChat { .. } => { + to_ids = pgp_to_ids; + + if let Some(chat_id) = chat_id { + past_ids = lookup_pgp_contacts_by_address_list( + context, + &mime_parser.past_members, + past_member_fingerprints, + Some(chat_id), + ) + .await?; + } else { + past_ids = add_or_lookup_pgp_contacts_by_address_list( + context, + &mime_parser.past_members, + &mime_parser.gossiped_keys, + past_member_fingerprints, + Origin::Hidden, + ) + .await?; + } + } + ChatAssignment::Trash | ChatAssignment::MailingList => { + to_ids = Vec::new(); + past_ids = Vec::new(); + } + ChatAssignment::ExistingChat { chat_id, .. } => { + let chat = Chat::load_from_db(context, *chat_id).await?; + if chat.is_encrypted(context).await? { + to_ids = pgp_to_ids; + past_ids = lookup_pgp_contacts_by_address_list( + context, + &mime_parser.past_members, + past_member_fingerprints, + Some(*chat_id), + ) + .await?; + } else { + to_ids = add_or_lookup_contacts_by_address_list( + context, + &mime_parser.recipients, + if !mime_parser.incoming { + Origin::OutgoingTo + } else if incoming_origin.is_known() { + Origin::IncomingTo + } else { + Origin::IncomingUnknownTo + }, + ) + .await?; + + past_ids = add_or_lookup_contacts_by_address_list( + context, + &mime_parser.past_members, + Origin::Hidden, + ) + .await?; + } + } + ChatAssignment::AdHocGroup => { + to_ids = add_or_lookup_contacts_by_address_list( + context, + &mime_parser.recipients, + if !mime_parser.incoming { + Origin::OutgoingTo + } else if incoming_origin.is_known() { + Origin::IncomingTo + } else { + Origin::IncomingUnknownTo + }, + ) + .await?; + + past_ids = add_or_lookup_contacts_by_address_list( + context, + &mime_parser.past_members, + Origin::Hidden, + ) + .await?; + } + ChatAssignment::OneOneChat => { + if pgp_to_ids + .first() + .is_some_and(|contact_id| contact_id.is_some()) + { + // There is a single recipient and we have + // mapped it to a PGP contact. + // This is a 1:1 PGP-chat. + to_ids = pgp_to_ids + } else if let Some(chat_id) = chat_id { + to_ids = lookup_pgp_contacts_by_address_list( + context, + &mime_parser.recipients, + to_member_fingerprints, + Some(chat_id), + ) + .await?; + } else { + let ids = match mime_parser.was_encrypted() { + true => { + lookup_pgp_contacts_by_address_list( + context, + &mime_parser.recipients, + to_member_fingerprints, + chat_id, + ) + .await? + } + false => vec![], + }; + if chat_id.is_some() + || (mime_parser.was_encrypted() && !ids.contains(&None)) + // Prefer creating PGP chats if there are any PGP contacts. At least this prevents + // from replying unencrypted. + || ids + .iter() + .any(|&c| c.is_some() && c != Some(ContactId::SELF)) + { + to_ids = ids; + } else { + to_ids = add_or_lookup_contacts_by_address_list( + context, + &mime_parser.recipients, + if !mime_parser.incoming { + Origin::OutgoingTo + } else if incoming_origin.is_known() { + Origin::IncomingTo + } else { + Origin::IncomingUnknownTo + }, + ) + .await?; + } + } + + past_ids = add_or_lookup_contacts_by_address_list( + context, + &mime_parser.past_members, + Origin::Hidden, + ) + .await?; + } + }; + + Ok((to_ids, past_ids)) +} + /// Receive a message and add it to the database. /// /// Returns an error on database failure or if the message is broken, @@ -427,232 +664,16 @@ pub(crate) async fn receive_imf_inner( .await?; info!(context, "Chat assignment is {chat_assignment:?}."); - // ID of the chat to look up the addresses in. - // - // Note that this is not necessarily the chat we want to assign the message to. - // In case of an outgoing private reply to a group message we may - // lookup the address of receipient in the list of addresses used in the group, - // but want to assign the message to 1:1 chat. - let chat_id = match chat_assignment { - ChatAssignment::Trash => None, - ChatAssignment::GroupChat { ref grpid } => { - if let Some((chat_id, _protected, _blocked)) = - chat::get_chat_id_by_grpid(context, grpid).await? - { - Some(chat_id) - } else { - None - } - } - ChatAssignment::AdHocGroup => { - // If we are going to assign a message to ad hoc group, - // we can just convert the email addresses - // to e-mail address contacts and don't need a `ChatId` - // to lookup PGP-contacts. - None - } - ChatAssignment::ExistingChat { chat_id, .. } => Some(chat_id), - ChatAssignment::MailingList => None, - ChatAssignment::OneOneChat => { - if is_partial_download.is_none() && !mime_parser.incoming { - parent_message.as_ref().map(|m| m.chat_id) - } else { - None - } - } - }; - - let member_fingerprints = mime_parser.chat_group_member_fingerprints(); - let to_member_fingerprints; - let past_member_fingerprints; - - if !member_fingerprints.is_empty() { - if member_fingerprints.len() >= mime_parser.recipients.len() { - (to_member_fingerprints, past_member_fingerprints) = - member_fingerprints.split_at(mime_parser.recipients.len()); - } else { - warn!( - context, - "Unexpected length of the fingerprint header, expected at least {}, got {}.", - mime_parser.recipients.len(), - member_fingerprints.len() - ); - to_member_fingerprints = &[]; - past_member_fingerprints = &[]; - } - } else { - to_member_fingerprints = &[]; - past_member_fingerprints = &[]; - } - - let pgp_to_ids = add_or_lookup_pgp_contacts_by_address_list( + let (to_ids, past_ids) = get_to_and_past_contact_ids( context, - &mime_parser.recipients, - &mime_parser.gossiped_keys, - to_member_fingerprints, - Origin::Hidden, + &mime_parser, + &chat_assignment, + is_partial_download, + &parent_message, + incoming_origin, ) .await?; - // `None` means that the chat is encrypted, - // but we were not able to convert the address - // to PGP-contact, e.g. - // because there wase no corresponding - // Autocrypt-Gossip header. - // - // This way we still preserve remaining - // contact number and positions - // so we can match them contacts to - // e.g. Chat-Group-Member-Timestamps - // header. - let to_ids: Vec>; - let past_ids: Vec>; - - match chat_assignment { - ChatAssignment::GroupChat { .. } => { - to_ids = pgp_to_ids; - - if let Some(chat_id) = chat_id { - past_ids = lookup_pgp_contacts_by_address_list( - context, - &mime_parser.past_members, - past_member_fingerprints, - Some(chat_id), - ) - .await?; - } else { - past_ids = add_or_lookup_pgp_contacts_by_address_list( - context, - &mime_parser.past_members, - &mime_parser.gossiped_keys, - past_member_fingerprints, - Origin::Hidden, - ) - .await?; - } - } - ChatAssignment::Trash | ChatAssignment::MailingList => { - to_ids = Vec::new(); - past_ids = Vec::new(); - } - ChatAssignment::ExistingChat { chat_id, .. } => { - let chat = Chat::load_from_db(context, chat_id).await?; - if chat.is_encrypted(context).await? { - to_ids = pgp_to_ids; - past_ids = lookup_pgp_contacts_by_address_list( - context, - &mime_parser.past_members, - past_member_fingerprints, - Some(chat_id), - ) - .await?; - } else { - to_ids = add_or_lookup_contacts_by_address_list( - context, - &mime_parser.recipients, - if !mime_parser.incoming { - Origin::OutgoingTo - } else if incoming_origin.is_known() { - Origin::IncomingTo - } else { - Origin::IncomingUnknownTo - }, - ) - .await?; - - past_ids = add_or_lookup_contacts_by_address_list( - context, - &mime_parser.past_members, - Origin::Hidden, - ) - .await?; - } - } - ChatAssignment::AdHocGroup => { - to_ids = add_or_lookup_contacts_by_address_list( - context, - &mime_parser.recipients, - if !mime_parser.incoming { - Origin::OutgoingTo - } else if incoming_origin.is_known() { - Origin::IncomingTo - } else { - Origin::IncomingUnknownTo - }, - ) - .await?; - - past_ids = add_or_lookup_contacts_by_address_list( - context, - &mime_parser.past_members, - Origin::Hidden, - ) - .await?; - } - ChatAssignment::OneOneChat => { - if pgp_to_ids - .first() - .is_some_and(|contact_id| contact_id.is_some()) - { - // There is a single recipient and we have - // mapped it to a PGP contact. - // This is a 1:1 PGP-chat. - to_ids = pgp_to_ids - } else if let Some(chat_id) = chat_id { - to_ids = lookup_pgp_contacts_by_address_list( - context, - &mime_parser.recipients, - to_member_fingerprints, - Some(chat_id), - ) - .await?; - } else { - let ids = match mime_parser.was_encrypted() { - true => { - lookup_pgp_contacts_by_address_list( - context, - &mime_parser.recipients, - to_member_fingerprints, - chat_id, - ) - .await? - } - false => vec![], - }; - if chat_id.is_some() - || (mime_parser.was_encrypted() && !ids.contains(&None)) - // Prefer creating PGP chats if there are any PGP contacts. At least this prevents - // from replying unencrypted. - || ids - .iter() - .any(|&c| c.is_some() && c != Some(ContactId::SELF)) - { - to_ids = ids; - } else { - to_ids = add_or_lookup_contacts_by_address_list( - context, - &mime_parser.recipients, - if !mime_parser.incoming { - Origin::OutgoingTo - } else if incoming_origin.is_known() { - Origin::IncomingTo - } else { - Origin::IncomingUnknownTo - }, - ) - .await?; - } - } - - past_ids = add_or_lookup_contacts_by_address_list( - context, - &mime_parser.past_members, - Origin::Hidden, - ) - .await?; - } - }; - let received_msg; if mime_parser.get_header(HeaderDef::SecureJoin).is_some() { let res = if mime_parser.incoming { From 167dd26bcf83820cc88d1a2b880c14c982c62ca4 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 25 Jun 2025 11:20:38 +0000 Subject: [PATCH 371/381] comment fixes --- src/receive_imf.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index ba8c9322a0..6a3ddfda6e 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -225,12 +225,12 @@ async fn get_to_and_past_contact_ids( // `None` means that the chat is encrypted, // but we were not able to convert the address // to PGP-contact, e.g. - // because there wase no corresponding + // because there was no corresponding // Autocrypt-Gossip header. // // This way we still preserve remaining - // contact number and positions - // so we can match them contacts to + // number of contacts and their positions + // so we can match the contacts to // e.g. Chat-Group-Member-Timestamps // header. let to_ids: Vec>; From 3fd47dcd6790b8fcc205a25b3d968f69e9ef34e6 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 25 Jun 2025 11:26:58 +0000 Subject: [PATCH 372/381] extend test_get_contacts --- src/contact/contact_tests.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs index d9502f0706..da3de36083 100644 --- a/src/contact/contact_tests.rs +++ b/src/contact/contact_tests.rs @@ -66,6 +66,8 @@ async fn test_get_contacts() -> Result<()> { // Alice is not in the contacts yet. let contacts = Contact::get_all(&context.ctx, 0, Some("Alice")).await?; assert_eq!(contacts.len(), 0); + let contacts = Contact::get_all(&context.ctx, 0, Some("MyName")).await?; + assert_eq!(contacts.len(), 0); let id = context.add_or_lookup_contact_id(&alice).await; assert_ne!(id, ContactId::UNDEFINED); From ad8b322ce232b3d319b7e0e44c85dc5607f604cd Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 25 Jun 2025 11:58:16 +0000 Subject: [PATCH 373/381] add warnings for ignored securejoin messages --- src/securejoin.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/securejoin.rs b/src/securejoin.rs index 6ca22ad549..e13205e03e 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -518,6 +518,7 @@ pub(crate) async fn observe_securejoin_on_other_device( }; if !encrypted_and_signed(context, mime_message, &get_self_fingerprint(context).await?) { + warn!(context, "Observed SecureJoin message is not encrypted correctly."); return Ok(HandshakeMessage::Ignore); } @@ -525,16 +526,19 @@ pub(crate) async fn observe_securejoin_on_other_device( let addr = contact.get_addr().to_lowercase(); let Some(key) = mime_message.gossiped_keys.get(&addr) else { + warn!(context, "No gossip header for {addr} at step {step}."); return Ok(HandshakeMessage::Ignore); }; let Some(contact_fingerprint) = contact.fingerprint() else { // Not a PGP-contact, should not happen. + warn!(context, "Contact does not have a fingerprint."); return Ok(HandshakeMessage::Ignore); }; if key.dc_fingerprint() != contact_fingerprint { // Fingerprint does not match, ignore. + warn!(context, "Fingerprint does not match."); return Ok(HandshakeMessage::Ignore); } From 43fe5998493c307e185583ce6100de076227f603 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 25 Jun 2025 12:12:47 +0000 Subject: [PATCH 374/381] rustfmt --- src/securejoin.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/securejoin.rs b/src/securejoin.rs index e13205e03e..237f0a811b 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -518,7 +518,10 @@ pub(crate) async fn observe_securejoin_on_other_device( }; if !encrypted_and_signed(context, mime_message, &get_self_fingerprint(context).await?) { - warn!(context, "Observed SecureJoin message is not encrypted correctly."); + warn!( + context, + "Observed SecureJoin message is not encrypted correctly." + ); return Ok(HandshakeMessage::Ignore); } From b647868d904529d3136778e80c5fec171bd9e11f Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 25 Jun 2025 12:17:12 +0000 Subject: [PATCH 375/381] compress email contact png --- assets/icon-email-contact.png | Bin 3491 -> 2286 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/icon-email-contact.png b/assets/icon-email-contact.png index ecd289e928d630d986899f109e745b3a8382eaef..9262dbdac2586a35d5d305cb5aa3694423bf12bd 100644 GIT binary patch delta 2009 zcmbtVc~sKr8ZKOKn|UnFbn2Fwl}nbXgxhs29mz^hndXL*l4F_Rj9V)HF2|cT9+8Y( zP?5^OUC>fY6Ms{oQHs$b?vRRz8{&p2!e!3+BVZZfa` zz*tvf%a$$hZI>$Y4xP*kvjDap_w~7YJ|`z9(eo0Bm$jC@%XXnPoMrmp3g9i`F%E~4tCK?+XiA17YE}xp3diwNf zLqkJRQISL7OTI%pTS`8csx3tzObeZ`? ziV7-~Iy*Z{p-@^{S_%pZW@cudJ$p7iJv}%$I6gjJS65e9Sh%vXvbwtZ@ZrOyr6sXg zTwh=R=+UFvTA=pp*RN8kG(SJTyu5sIagj!&H8(fQWU{=xyv@x`Hk-}ma+ONuz`%e? zrD|<$l@7Cmw`|dl^FHT(HFnoVV zYH=_m+@YU0F0t+Ptk8V6@YE3|Mv|{O7$`6c{!n_f(<^H|=EVwdA9UN3oG4v8*LYZZ zDkw>XNeL(KUF(8))xvH3Dws5UBId5&15eVtb)ESjW@fd?@ffGqv#Zxj*BKA8N8?g{wcBUGDL8x4YCn!_7%1qEtIl_q zuG?QhUa~Ayi};}Y9!H?Vk{`ELxoE}ihq_fy^a9l1$hi02o4b?4um+bcmSk_psLxA^ zg5s@eXXPzu+l(>AMPMEId-}^E9ivEJ=i<<25u<6_uq`^hhNrIAc3GrX?yhavf|3nv z)ywA5ybpNbT|IX=)4@|Pr&I6TrB5b8!{?rI_x(fdSPyaQ&g{b6mBkHHo!x-~AckpI zv`}Z#S&}ej0TXFTnlwl)lwyuuXnQU7eKa`(qh_M58#GjMUp))i~eXs-f>)JTb>?bK$?>^igNitSUE zmLPm^nA>Ov_-MzFdZL)_`FhzXKAQ#(ZL>npk3>2gD|^mkct%aFYHTmH2wpW;j`u_| zFdKmb(!9b1 z+aRI~ChV+2NPr0x(KdM6bw7k>3`GpQ*SY_D@Y4EB2iZq(LZj=GI86)}1Zh|xf*s{H z-wWPrh%AHUby<)A18Bos!ttbePu*cI*f6muH0P#-V`u(O2X#a2u=*`GA!g_vO;17b z-*P~EgMHZBM{OZIpKwi}p7=i$|DPZU2SOyIx-X+=HJ;bTf2o-o_Y+t0xLrQtN=t4R z@e|-z_!J*XDeq&^`U7ENZ$+jfg)qvJ1YU>zgj8Vt%_zSxZSZbVY5h4 zH&TJ0ttlQyo4Y#bsg3cS6?o}EtM5uqkmxsNk=^%%sRjAd) g%eZr<`agUCZhyJ&{~M`*P=zsMInP~27~awKZ|e!^`2YX_ literal 3491 zcmeHKYc$*07XK^tD)oLmA zlBhwY9_h5IN`%{b1o0fCMZ7{#+F4g`|R^u=d9m4*&c2V zlH$k20RWJ6ylCqQ00L8AO%x0OJ{{AJ;BX@NqNghW;I#nYW(EN4f}xvi0EjaLfX_$( zFuxA~N>QcMOHeRzD9G8t7T^=}dpp3HSky%X4gik6{%QhU*y<24C=%=FYA5nZSVCIM zMB7>28UQ3*9c?eb6GoR=`#AWZTGtvD_G7`L48=5Yr;8`V!|k>6X10N!BwxEMzm7%g z7~kRY*Yc~4(mzzGe^gO>?%{Dpo|0C7UQ9Mk(gUI^Er-7e8sP(U$!x;I* zvd8D)sbS=+Tj~AAE+bO|>HDOx7p}IcVXgfSIFY=s%?L0>cajEsb3@H zSNz;hyuM7x%$GozRMdx=NzdH}%=8Y#F?>czkl5Zw19eeLSYc(yE}SiM{g!4F&fzj! z=K1Bp0eO7J;*2Pipz(~17<_wfB4$Rkv_uAvf$)Xjls>Iyl=6vRIHrPYBxSGpoH$!w^T;v-nnXtHt+g3pqrcUvmrg~$nlzAOfQ z-5Z3ZeI8`EAF)gic#}$hV6tMSJPffT9&S<3giQNCK8iLT(xdZDxzfC{)t!6M2BBSQ zBj{;WkuAH#Hy>LMUiE9p%Mg%H5e>g?Sd)mW)r5!}Y?+)KD+;sKHGp$W%rc{AJ+6`D zpsk-uyD~@(Ct%qpIi3#*P_Agj#gi1~Qa~J*AHY5-4k!En{5D@d?_2s+$RtYvpEU*@ zmD#Kxz<7$G8(U6|kuxAOS)UmDnw;SBu{RV*VwDZ&2tg6+uh5m>xD$`*yU?d5Zdq(| zn>=;I;^B=~jRodOten18r8s&bGYNebkDScuZ!mkKJo47cdSH6*)+psuLtA-Zm~Z<~ z+Y*6l+~;-ulCa4l|K~3!8mP6W_;Iur)WSG{ODwmHfJ7i0G^)m(*_&iFrkQ+rmYp`b(E*VYOh%W?RGC9cpoXU8tvZ z<4`+$&o5~9Ze9Ag>atg|U-f{KYz z%n55YHCI=64&?o?mx@8B#1nDDGvkg3?JbDvFP~hdcXNdfmG~W$6DFfCM+SFV&-5-P zjO=W`o0PJ}Un}>JO{G)uLk7{72OgP9Igz7faRy!7`C!E8nJo$MQa^sWa>I%Zk0m9L zzG&049$zhOuYw;ciA!0wfEEQD?NXPpY&kgD%f=B$|gFIv7?A?hu% zhIvOdagKQiPmr90i6m;&ZmxSsy9cyqzY<&c(JngsaMNAZX_LMFO%a5Fytn0Y%Rg3Q zf{Gfo6i1eN$2cD#OnrIAa>ARPZJ*VB;ZSEiO5XAca|&uizfO8lSxZl9naadW$4lJX z4*{EP&-H!izX)wE7=W$)Ub^8&v*%StgJ(=j#k4LK5Efh%k91D-9RBi6N8f-(Ck?fq zvQ@fue-&0Arj#w2@>B1VJXi#HGpzQVau?UK_2e1&PMprYTc&;rcdsE;w`n*GM@TUz zso66d|KYMsx9}F0S%YZcwhv7&%tLvFw+UcWOr&Xg?-EaBqJ^A-*!rZkQVZYRsDfD! zWM4F+ASM-gg|#(9m1+U^HdC(lPg|Mfq=29Z5wO&Q&5fAkdVGGKzS}?R9=&&jQkXln zWzO|IKH6I>r&HOk-gcVmBORenO?V*(o?`rTbDlsEvl!!8HmY5ajN}*U5ti~~|J|Qs zC|ERuhVnZ-?p>o0puIKWG#7G~CwE`j9;uoz*}#7s_<_{1V=YX`&yyXEy)2|jN$I(= zS);WAtLI%gWE0k98WT8 ziD+9G(~uhjYkW(|Te6xysPJuG%eE*?Pi*#d#DZ;9(@85bgtNc5SMF(->5q|Dh6t5v0%9@=IUqoOiHOJ)4COBi@cn&QK0-FRQan;Y?)r&LfX&= zF&*fH%4>)4+*x=PO`iX) z-)p=%GQFtJ|JLGckS!T$+^~jQfaL>nbfWGPiNjf5$a86B`m38)_aL6ABK1k)hG|dWI%?-x7}h439VI3}RHiV!W`Z*!aMhP#``&UO(b`BrZ5G ZDpVgE6IQ~39s?Hvj&^RgbueVwzW{64IFSGV From 3a32cc7eeb77d5adab49d89ccc7040e1930a4bc5 Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Thu, 26 Jun 2025 11:57:17 +0200 Subject: [PATCH 376/381] reword pgp_contacts to key_contacts --- deltachat-ffi/deltachat.h | 2 +- deltachat-jsonrpc/src/api.rs | 2 +- deltachat-jsonrpc/src/api/types/chat.rs | 12 +-- deltachat-jsonrpc/src/api/types/chat_list.rs | 6 +- deltachat-jsonrpc/src/api/types/contact.rs | 10 +-- deltachat-rpc-client/tests/test_something.py | 2 +- python/tests/test_3_offline.py | 2 +- src/chat.rs | 16 ++-- src/chat/chat_tests.rs | 12 +-- src/contact.rs | 20 ++--- src/contact/contact_tests.rs | 8 +- src/headerdef.rs | 2 +- src/mimefactory.rs | 8 +- src/receive_imf.rs | 68 ++++++++-------- src/receive_imf/receive_imf_tests.rs | 24 +++--- src/securejoin.rs | 2 +- src/securejoin/securejoin_tests.rs | 2 +- src/sql/migrations.rs | 82 ++++++++++---------- src/sql/migrations/migrations_tests.rs | 8 +- src/test_utils.rs | 10 +-- src/tests/aeap.rs | 8 +- 21 files changed, 153 insertions(+), 153 deletions(-) diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 53891ab08e..233f192427 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -3841,7 +3841,7 @@ int dc_chat_is_protected (const dc_chat_t* chat); /** * Check if the chat is encrypted. * - * 1:1 chats with PGP-contacts and group chats with PGP-contacts + * 1:1 chats with key-contacts and group chats with key-contacts * are encrypted. * 1:1 chats with emails contacts and ad-hoc groups * created for email threads are not encrypted. diff --git a/deltachat-jsonrpc/src/api.rs b/deltachat-jsonrpc/src/api.rs index c63861b032..558643df6c 100644 --- a/deltachat-jsonrpc/src/api.rs +++ b/deltachat-jsonrpc/src/api.rs @@ -358,7 +358,7 @@ impl CommandApi { /// and migrated to the current version, /// then this function returns it. /// - /// This function is useful because the PGP-contacts migration could fail due to bugs + /// This function is useful because the key-contacts migration could fail due to bugs /// and then the account will not work properly. /// /// After opening an account, the UI should call this function diff --git a/deltachat-jsonrpc/src/api/types/chat.rs b/deltachat-jsonrpc/src/api/types/chat.rs index 3e90d63b24..77f2f1f5b3 100644 --- a/deltachat-jsonrpc/src/api/types/chat.rs +++ b/deltachat-jsonrpc/src/api/types/chat.rs @@ -32,8 +32,8 @@ pub struct FullChat { is_protected: bool, /// True if the chat is encrypted. /// This means that all messages in the chat are encrypted, - /// and all contacts in the chat are "pgp-contacts", - /// i.e. identified by the PGP key fingerprint. + /// and all contacts in the chat are "key-contacts", + /// i.e. identified by the fingerprint. /// /// False if the chat is unencrypted. /// This means that all messages in the chat are unencrypted, @@ -51,7 +51,7 @@ pub struct FullChat { /// contains unencrypted messages that were received in core <= v1.159.* /// and vice versa. /// - /// See also `is_pgp_contact` on `Contact`. + /// See also `is_key_contact` on `Contact`. is_encrypted: bool, profile_image: Option, //BLOBS ? archived: bool, @@ -186,8 +186,8 @@ pub struct BasicChat { /// True if the chat is encrypted. /// This means that all messages in the chat are encrypted, - /// and all contacts in the chat are "pgp-contacts", - /// i.e. identified by the PGP key fingerprint. + /// and all contacts in the chat are "key-contacts", + /// i.e. identified by the fingerprint. /// /// False if the chat is unencrypted. /// This means that all messages in the chat are unencrypted, @@ -205,7 +205,7 @@ pub struct BasicChat { /// contains unencrypted messages that were received in core <= v1.159.* /// and vice versa. /// - /// See also `is_pgp_contact` on `Contact`. + /// See also `is_key_contact` on `Contact`. is_encrypted: bool, profile_image: Option, //BLOBS ? archived: bool, diff --git a/deltachat-jsonrpc/src/api/types/chat_list.rs b/deltachat-jsonrpc/src/api/types/chat_list.rs index 285de89f81..3e2ae28413 100644 --- a/deltachat-jsonrpc/src/api/types/chat_list.rs +++ b/deltachat-jsonrpc/src/api/types/chat_list.rs @@ -33,8 +33,8 @@ pub enum ChatListItemFetchResult { /// True if the chat is encrypted. /// This means that all messages in the chat are encrypted, - /// and all contacts in the chat are "pgp-contacts", - /// i.e. identified by the PGP key fingerprint. + /// and all contacts in the chat are "key-contacts", + /// i.e. identified by the fingerprint. /// /// False if the chat is unencrypted. /// This means that all messages in the chat are unencrypted, @@ -52,7 +52,7 @@ pub enum ChatListItemFetchResult { /// contains unencrypted messages that were received in core <= v1.159.* /// and vice versa. /// - /// See also `is_pgp_contact` on `Contact`. + /// See also `is_key_contact` on `Contact`. is_encrypted: bool, is_group: bool, fresh_message_counter: usize, diff --git a/deltachat-jsonrpc/src/api/types/contact.rs b/deltachat-jsonrpc/src/api/types/contact.rs index 5c367012ff..3d8ce98df6 100644 --- a/deltachat-jsonrpc/src/api/types/contact.rs +++ b/deltachat-jsonrpc/src/api/types/contact.rs @@ -20,13 +20,13 @@ pub struct ContactObject { name_and_addr: String, is_blocked: bool, - /// Is the contact a PGP contact. - is_pgp_contact: bool, + /// Is the contact a key contact. + is_key_contact: bool, /// Is encryption available for this contact. /// - /// This can only be true for PGP-contacts. - /// However, it is possible to have a PGP-contact + /// This can only be true for key-contacts. + /// However, it is possible to have a key-contact /// for which encryption is not available because we don't have a key yet, /// e.g. if we just scanned the fingerprint from a QR code. e2ee_avail: bool, @@ -91,7 +91,7 @@ impl ContactObject { profile_image, //BLOBS name_and_addr: contact.get_name_n_addr(), is_blocked: contact.is_blocked(), - is_pgp_contact: contact.is_pgp_contact(), + is_key_contact: contact.is_key_contact(), e2ee_avail: contact.e2ee_avail(context).await?, is_verified, is_profile_verified, diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index f538699c68..b87db8ff2e 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -170,7 +170,7 @@ def test_account(acfactory) -> None: assert alice.get_size() assert alice.is_configured() assert not alice.get_avatar() - assert alice.get_contact_by_addr(bob_addr) is None # There is no email-contact, only PGP-contact + assert alice.get_contact_by_addr(bob_addr) is None # There is no email-contact, only key-contact assert alice.get_contacts() assert alice.get_contacts(snapshot=True) assert alice.self_contact diff --git a/python/tests/test_3_offline.py b/python/tests/test_3_offline.py index c76b2ddca9..d991dde347 100644 --- a/python/tests/test_3_offline.py +++ b/python/tests/test_3_offline.py @@ -160,7 +160,7 @@ def test_delete_referenced_contact_hides_contact(self, acfactory): def test_create_chat_flexibility(self, acfactory): ac1 = acfactory.get_pseudo_configured_account() ac2 = acfactory.get_pseudo_configured_account() - chat1 = ac1.create_chat(ac2) # This creates a PGP-contact chat + chat1 = ac1.create_chat(ac2) # This creates a key-contact chat chat2 = ac1.create_chat(ac2.get_self_contact().addr) # This creates email-contact chat assert chat1 != chat2 ac3 = acfactory.get_unconfigured_account() diff --git a/src/chat.rs b/src/chat.rs index 349f22c10d..e713e46553 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -126,7 +126,7 @@ pub(crate) enum CantSendReason { /// Not a member of the chat. NotAMember, - /// State for 1:1 chat with a PGP-contact that does not have a key. + /// State for 1:1 chat with a key-contact that does not have a key. MissingKey, } @@ -147,7 +147,7 @@ impl fmt::Display for CantSendReason { write!(f, "mailing list does not have a know post address") } Self::NotAMember => write!(f, "not a member of the chat"), - Self::MissingKey => write!(f, "OpenPGP key is missing"), + Self::MissingKey => write!(f, "key is missing"), } } } @@ -1336,7 +1336,7 @@ impl ChatId { { let contact = Contact::get_by_id(context, *contact_id).await?; let addr = contact.get_addr(); - debug_assert!(contact.is_pgp_contact()); + debug_assert!(contact.is_key_contact()); let fingerprint = contact .fingerprint() .context("Contact does not have a fingerprint in encrypted chat")?; @@ -1671,7 +1671,7 @@ impl Chat { let contact_ids = get_chat_contacts(context, self.id).await?; if let Some(contact_id) = contact_ids.first() { let contact = Contact::get_by_id(context, *contact_id).await?; - if contact.is_pgp_contact() && contact.public_key(context).await?.is_none() { + if contact.is_key_contact() && contact.public_key(context).await?.is_none() { return Ok(Some(reason)); } } @@ -1861,7 +1861,7 @@ impl Chat { true } else { let contact = Contact::get_by_id(context, *contact_id).await?; - contact.is_pgp_contact() + contact.is_key_contact() } } else { true @@ -3824,8 +3824,8 @@ pub(crate) async fn add_contact_to_chat_ex( "Cannot add SELF to broadcast." ); ensure!( - chat.is_encrypted(context).await? == contact.is_pgp_contact(), - "Only PGP-contacts can be added to encrypted chats" + chat.is_encrypted(context).await? == contact.is_key_contact(), + "Only key-contacts can be added to encrypted chats" ); if !chat.is_self_in_chat(context).await? { @@ -4829,7 +4829,7 @@ async fn set_contacts_by_fingerprints( let chat = Chat::load_from_db(context, id).await?; ensure!( chat.is_encrypted(context).await?, - "Cannot add PGP-contacts to unencrypted chat {id}" + "Cannot add key-contacts to unencrypted chat {id}" ); ensure!( chat.typ == Chattype::Broadcast, diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 10c05cf969..dac48ec7a2 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -355,7 +355,7 @@ async fn test_member_add_remove() -> Result<()> { for contact_id in fiona_contact_ids { let contact = Contact::get_by_id(&fiona, contact_id).await?; assert_ne!(contact.get_name(), "robert"); - assert!(contact.is_pgp_contact()); + assert!(contact.is_key_contact()); } tcm.section("Alice removes Bob from the chat."); @@ -4241,7 +4241,7 @@ async fn test_oneone_gossip() -> Result<()> { let sent_msg3 = alice.send_text(alice_chat.id, "Hello again, Bob!").await; // This message has no Autocrypt-Gossip header, - // but should still be assigned to PGP-contact. + // but should still be assigned to key-contact. tcm.section("Alice receives a copy of another message on second device"); let rcvd_msg3 = alice2.recv_msg(&sent_msg3).await; assert_eq!(rcvd_msg3.get_showpadlock(), true); @@ -4266,7 +4266,7 @@ async fn test_no_email_contacts_in_group_chats() -> Result<()> { let pgp_bob_contact_id = alice.add_or_lookup_contact_id(bob).await; let email_charlie_contact_id = alice.add_or_lookup_email_contact_id(charlie).await; - // PGP-contact should be added successfully. + // key-contact should be added successfully. add_contact_to_chat(alice, chat_id, pgp_bob_contact_id).await?; // Adding email-contact should fail. @@ -4276,9 +4276,9 @@ async fn test_no_email_contacts_in_group_chats() -> Result<()> { Ok(()) } -/// Tests that PGP-contacts cannot be added to ad hoc groups. +/// Tests that key-contacts cannot be added to ad hoc groups. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_no_pgp_contacts_in_adhoc_chats() -> Result<()> { +async fn test_no_key_contacts_in_adhoc_chats() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = &tcm.alice().await; let bob = &tcm.bob().await; @@ -4305,7 +4305,7 @@ async fn test_no_pgp_contacts_in_adhoc_chats() -> Result<()> { // Email-contact should be added successfully. add_contact_to_chat(alice, chat_id, email_bob_contact_id).await?; - // Adding PGP-contact should fail. + // Adding key-contact should fail. let res = add_contact_to_chat(alice, chat_id, pgp_charlie_contact_id).await; assert!(res.is_err()); diff --git a/src/contact.rs b/src/contact.rs index bbb33c47b8..f3950dfe9e 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -436,7 +436,7 @@ pub struct Contact { addr: String, /// OpenPGP key fingerprint. - /// Non-empty iff the contact is a pgp-contact, + /// Non-empty iff the contact is a key-contact, /// identified by this fingerprint. fingerprint: Option, @@ -795,7 +795,7 @@ impl Contact { .query_get_value( "SELECT id FROM contacts WHERE addr=?1 COLLATE NOCASE - AND fingerprint='' -- Do not lookup PGP-contacts + AND fingerprint='' -- Do not lookup key-contacts AND id>?2 AND origin>=?3 AND (? OR blocked=?)", ( &addr_normalized, @@ -819,7 +819,7 @@ impl Contact { } /// Lookup a contact and create it if it does not exist yet. - /// If `fingerprint` is non-empty, a PGP-contact with this fingerprint is added / looked up. + /// If `fingerprint` is non-empty, a key-contact with this fingerprint is added / looked up. /// Otherwise, an email-contact with `addr` is added / looked up. /// A name and an "origin" can be given. /// @@ -1384,9 +1384,9 @@ impl Contact { &self.addr } - /// Returns true if the contact is a PGP-contact. + /// Returns true if the contact is a key-contact. /// Otherwise it is an email contact. - pub fn is_pgp_contact(&self) -> bool { + pub fn is_key_contact(&self) -> bool { self.fingerprint.is_some() } @@ -1403,9 +1403,9 @@ impl Contact { /// Returns OpenPGP public key of a contact. /// - /// Returns `None` if the contact is not a PGP-contact + /// Returns `None` if the contact is not a key-contact /// or if the key is not available. - /// It is possible for a PGP-contact to not have a key, + /// It is possible for a key-contact to not have a key, /// e.g. if only the fingerprint is known from a QR-code. pub async fn public_key(&self, context: &Context) -> Result> { if self.id == ContactId::SELF { @@ -1520,7 +1520,7 @@ impl Contact { } else if self.id == ContactId::DEVICE { return Ok(Some(chat::get_device_icon(context).await?)); } - if show_fallback_icon && !self.id.is_special() && !self.is_pgp_contact() { + if show_fallback_icon && !self.id.is_special() && !self.is_key_contact() { return Ok(Some(chat::get_email_contact_icon(context).await?)); } if let Some(image_rel) = self.param.get(Param::ProfileImage) { @@ -1918,7 +1918,7 @@ pub(crate) async fn mark_contact_id_as_verified( |row| row.get(0), )?; if contact_fingerprint.is_empty() { - bail!("Non-PGP contact {contact_id} cannot be verified"); + bail!("Non-key-contact {contact_id} cannot be verified"); } if verifier_id != ContactId::SELF { let verifier_fingerprint: String = transaction.query_row( @@ -1928,7 +1928,7 @@ pub(crate) async fn mark_contact_id_as_verified( )?; if verifier_fingerprint.is_empty() { bail!( - "Contact {contact_id} cannot be verified by non-PGP contact {verifier_id}" + "Contact {contact_id} cannot be verified by non-key-contact {verifier_id}" ); } } diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs index da3de36083..51c0b68eb3 100644 --- a/src/contact/contact_tests.rs +++ b/src/contact/contact_tests.rs @@ -1222,7 +1222,7 @@ async fn test_self_is_verified() -> Result<()> { assert_eq!(contact.is_verified(&alice).await?, true); assert!(contact.is_profile_verified(&alice).await?); assert!(contact.get_verifier_id(&alice).await?.is_none()); - assert!(contact.is_pgp_contact()); + assert!(contact.is_key_contact()); let chat_id = ChatId::get_for_contact(&alice, ContactId::SELF).await?; assert!(chat_id.is_protected(&alice).await.unwrap() == ProtectionStatus::Protected); @@ -1230,9 +1230,9 @@ async fn test_self_is_verified() -> Result<()> { Ok(()) } -/// Tests that importing a vCard with a key creates a PGP-contact. +/// Tests that importing a vCard with a key creates a key-contact. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_vcard_creates_pgp_contact() -> Result<()> { +async fn test_vcard_creates_key_contact() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = &tcm.alice().await; let bob = &tcm.bob().await; @@ -1242,7 +1242,7 @@ async fn test_vcard_creates_pgp_contact() -> Result<()> { assert_eq!(contact_ids.len(), 1); let contact_id = contact_ids.first().unwrap(); let contact = Contact::get_by_id(alice, *contact_id).await?; - assert!(contact.is_pgp_contact()); + assert!(contact.is_key_contact()); Ok(()) } diff --git a/src/headerdef.rs b/src/headerdef.rs index 330a4d9ba0..91d939d575 100644 --- a/src/headerdef.rs +++ b/src/headerdef.rs @@ -75,7 +75,7 @@ pub enum HeaderDef { /// for members listed in the `Chat-Group-Past-Members` field. ChatGroupMemberTimestamps, - /// Space-separated PGP key fingerprints + /// Space-separated fingerprints /// of group members listed in the `To` field /// followed by fingerprints /// of past members listed in the `Chat-Group-Past-Members` field. diff --git a/src/mimefactory.rs b/src/mimefactory.rs index db9768f844..d301b2a53f 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -310,7 +310,7 @@ impl MimeFactory { } else if id == ContactId::SELF { member_fingerprints.push(self_fingerprint.to_string()); } else { - debug_assert!(member_fingerprints.is_empty(), "If some past member is a PGP-contact, all other past members should be PGP-contacts too"); + debug_assert!(member_fingerprints.is_empty(), "If some past member is a key-contact, all other past members should be key-contacts too"); } } member_timestamps.push(add_timestamp); @@ -361,7 +361,7 @@ impl MimeFactory { // if we are leaving the group. past_member_fingerprints.push(self_fingerprint.to_string()); } else { - debug_assert!(past_member_fingerprints.is_empty(), "If some past member is a PGP-contact, all other past members should be PGP-contacts too"); + debug_assert!(past_member_fingerprints.is_empty(), "If some past member is a key-contact, all other past members should be key-contacts too"); } } } @@ -486,7 +486,7 @@ impl MimeFactory { let timestamp = create_smeared_timestamp(context); let addr = contact.get_addr().to_string(); - let encryption_keys = if contact.is_pgp_contact() { + let encryption_keys = if contact.is_key_contact() { if let Some(key) = contact.public_key(context).await? { Some(vec![(addr.clone(), key)]) } else { @@ -1357,7 +1357,7 @@ impl MimeFactory { } SystemMessage::MemberAddedToGroup => { // TODO: lookup the contact by ID rather than email address. - // We are adding PGP contacts, the cannot be looked up by address. + // We are adding key-contacts, the cannot be looked up by address. let email_to_add = msg.param.get(Param::Arg).unwrap_or_default(); placeholdertext = Some(stock_str::msg_add_member_remote(context, email_to_add).await); diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 6a3ddfda6e..8aa3b912fa 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -75,12 +75,12 @@ pub struct ReceivedMsg { /// /// This is done before looking up contact IDs /// so we know in advance whether to lookup -/// PGP-contacts or email address contacts. +/// key-contacts or email address contacts. /// /// Once this decision is made, /// it should not be changed so we /// don't assign the message to an encrypted -/// group after looking up PGP-contacts +/// group after looking up key-contacts /// or vice versa. #[derive(Debug)] enum ChatAssignment { @@ -89,7 +89,7 @@ enum ChatAssignment { /// Group chat with a Group ID. /// - /// Lookup PGP contacts and + /// Lookup key-contacts and /// assign to encrypted group. GroupChat { grpid: String }, @@ -106,7 +106,7 @@ enum ChatAssignment { /// and no contact IDs should be looked /// up except the `from_id` /// which may be an email address contact - /// or a PGP-contact. + /// or a key-contact. MailingList, /// Group chat without a Group ID. @@ -137,7 +137,7 @@ enum ChatAssignment { /// it does not matter. /// It is not possible to mix /// email address contacts - /// with PGP-contacts in a single 1:1 chat anyway. + /// with key-contacts in a single 1:1 chat anyway. OneOneChat, } @@ -224,7 +224,7 @@ async fn get_to_and_past_contact_ids( ) -> Result<(Vec>, Vec>)> { // `None` means that the chat is encrypted, // but we were not able to convert the address - // to PGP-contact, e.g. + // to key-contact, e.g. // because there was no corresponding // Autocrypt-Gossip header. // @@ -257,7 +257,7 @@ async fn get_to_and_past_contact_ids( // If we are going to assign a message to ad hoc group, // we can just convert the email addresses // to e-mail address contacts and don't need a `ChatId` - // to lookup PGP-contacts. + // to lookup key-contacts. None } ChatAssignment::ExistingChat { chat_id, .. } => Some(*chat_id), @@ -294,7 +294,7 @@ async fn get_to_and_past_contact_ids( past_member_fingerprints = &[]; } - let pgp_to_ids = add_or_lookup_pgp_contacts_by_address_list( + let pgp_to_ids = add_or_lookup_key_contacts_by_address_list( context, &mime_parser.recipients, &mime_parser.gossiped_keys, @@ -308,7 +308,7 @@ async fn get_to_and_past_contact_ids( to_ids = pgp_to_ids; if let Some(chat_id) = chat_id { - past_ids = lookup_pgp_contacts_by_address_list( + past_ids = lookup_key_contacts_by_address_list( context, &mime_parser.past_members, past_member_fingerprints, @@ -316,7 +316,7 @@ async fn get_to_and_past_contact_ids( ) .await?; } else { - past_ids = add_or_lookup_pgp_contacts_by_address_list( + past_ids = add_or_lookup_key_contacts_by_address_list( context, &mime_parser.past_members, &mime_parser.gossiped_keys, @@ -334,7 +334,7 @@ async fn get_to_and_past_contact_ids( let chat = Chat::load_from_db(context, *chat_id).await?; if chat.is_encrypted(context).await? { to_ids = pgp_to_ids; - past_ids = lookup_pgp_contacts_by_address_list( + past_ids = lookup_key_contacts_by_address_list( context, &mime_parser.past_members, past_member_fingerprints, @@ -390,11 +390,11 @@ async fn get_to_and_past_contact_ids( .is_some_and(|contact_id| contact_id.is_some()) { // There is a single recipient and we have - // mapped it to a PGP contact. - // This is a 1:1 PGP-chat. + // mapped it to a key contact. + // This is a 1:1 key-chat. to_ids = pgp_to_ids } else if let Some(chat_id) = chat_id { - to_ids = lookup_pgp_contacts_by_address_list( + to_ids = lookup_key_contacts_by_address_list( context, &mime_parser.recipients, to_member_fingerprints, @@ -404,7 +404,7 @@ async fn get_to_and_past_contact_ids( } else { let ids = match mime_parser.was_encrypted() { true => { - lookup_pgp_contacts_by_address_list( + lookup_key_contacts_by_address_list( context, &mime_parser.recipients, to_member_fingerprints, @@ -416,7 +416,7 @@ async fn get_to_and_past_contact_ids( }; if chat_id.is_some() || (mime_parser.was_encrypted() && !ids.contains(&None)) - // Prefer creating PGP chats if there are any PGP contacts. At least this prevents + // Prefer creating PGP chats if there are any key-contacts. At least this prevents // from replying unencrypted. || ids .iter() @@ -642,7 +642,7 @@ pub(crate) async fn receive_imf_inner( // when a message is sent by Thunderbird. // // This can be also used to lookup - // PGP-contact by email address + // key-contact by email address // when receiving a private 1:1 reply // to a group chat message. let parent_message = get_parent_message( @@ -1005,9 +1005,9 @@ pub(crate) async fn receive_imf_inner( /// display names. We don't want the display name to change every time the user gets a new email from /// a mailing list. /// -/// * `find_pgp_contact_by_addr`: if true, we only know the e-mail address +/// * `find_key_contact_by_addr`: if true, we only know the e-mail address /// of the contact, but not the fingerprint, -/// yet want to assign the message to some PGP-contact. +/// yet want to assign the message to some key-contact. /// This can happen during prefetch or when the message is partially downloaded. /// If we get it wrong, the message will be placed into the correct /// chat after downloading. @@ -1018,7 +1018,7 @@ pub async fn from_field_to_contact_id( from: &SingleInfo, fingerprint: Option<&Fingerprint>, prevent_rename: bool, - find_pgp_contact_by_addr: bool, + find_key_contact_by_addr: bool, ) -> Result> { let fingerprint = fingerprint.as_ref().map(|fp| fp.hex()).unwrap_or_default(); let display_name = if prevent_rename { @@ -1037,16 +1037,16 @@ pub async fn from_field_to_contact_id( } }; - if fingerprint.is_empty() && find_pgp_contact_by_addr { + if fingerprint.is_empty() && find_key_contact_by_addr { let addr_normalized = addr_normalize(&from_addr); - // Try to assign to some PGP-contact. + // Try to assign to some key-contact. if let Some((from_id, origin)) = context .sql .query_row_optional( "SELECT id, origin FROM contacts WHERE addr=?1 COLLATE NOCASE - AND fingerprint<>'' -- Only PGP-contacts + AND fingerprint<>'' -- Only key-contacts AND id>?2 AND origin>=?3 AND blocked=?4 ORDER BY last_seen DESC LIMIT 1", @@ -2833,7 +2833,7 @@ async fn apply_group_changes( // rather than old display name. // This could be fixed by looking up the contact with the highest // `remove_timestamp` after applying Chat-Group-Member-Timestamps. - removed_id = lookup_pgp_contact_by_address(context, removed_addr, Some(chat_id)).await?; + removed_id = lookup_key_contact_by_address(context, removed_addr, Some(chat_id)).await?; if let Some(id) = removed_id { better_msg = if id == from_id { silent = true; @@ -2854,7 +2854,7 @@ async fn apply_group_changes( // may contain display name of the wrong contact. let fingerprint = key.dc_fingerprint().hex(); if let Some(contact_id) = - lookup_pgp_contact_by_fingerprint(context, &fingerprint).await? + lookup_key_contact_by_fingerprint(context, &fingerprint).await? { added_id = Some(contact_id); better_msg = @@ -3582,7 +3582,7 @@ async fn add_or_lookup_contacts_by_address_list( } /// Looks up contact IDs from the database given the list of recipients. -async fn add_or_lookup_pgp_contacts_by_address_list( +async fn add_or_lookup_key_contacts_by_address_list( context: &Context, address_list: &[SingleInfo], gossiped_keys: &HashMap, @@ -3627,11 +3627,11 @@ async fn add_or_lookup_pgp_contacts_by_address_list( Ok(contact_ids) } -/// Looks up a PGP-contact by email address. +/// Looks up a key-contact by email address. /// -/// If provided, `chat_id` must be an encrypted chat ID that has PGP-contacts inside. +/// If provided, `chat_id` must be an encrypted chat ID that has key-contacts inside. /// Otherwise the function searches in all contacts, returning the recently seen one. -async fn lookup_pgp_contact_by_address( +async fn lookup_key_contact_by_address( context: &Context, addr: &str, chat_id: Option, @@ -3689,13 +3689,13 @@ async fn lookup_pgp_contact_by_address( Ok(contact_id) } -async fn lookup_pgp_contact_by_fingerprint( +async fn lookup_key_contact_by_fingerprint( context: &Context, fingerprint: &str, ) -> Result> { debug_assert!(!fingerprint.is_empty()); if fingerprint.is_empty() { - // Avoid accidentally looking up a non-PGP contact. + // Avoid accidentally looking up a non-key-contact. return Ok(None); } if let Some(contact_id) = context @@ -3723,7 +3723,7 @@ async fn lookup_pgp_contact_by_fingerprint( } } -/// Looks up PGP-contacts by email addresses. +/// Looks up key-contacts by email addresses. /// /// `fingerprints` may be empty. /// This is used as a fallback when email addresses are available, @@ -3738,7 +3738,7 @@ async fn lookup_pgp_contact_by_fingerprint( /// is the same as the number of addresses in the header /// and it is possible to find corresponding /// `Chat-Group-Member-Timestamps` items. -async fn lookup_pgp_contacts_by_address_list( +async fn lookup_key_contacts_by_address_list( context: &Context, address_list: &[SingleInfo], fingerprints: &[Fingerprint], @@ -3773,7 +3773,7 @@ async fn lookup_pgp_contacts_by_address_list( contact_ids.push(None); } } else { - let contact_id = lookup_pgp_contact_by_address(context, addr, chat_id).await?; + let contact_id = lookup_key_contact_by_address(context, addr, chat_id).await?; contact_ids.push(contact_id); } } diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 6f2663b639..dcdd8f13ee 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -3249,7 +3249,7 @@ async fn test_outgoing_undecryptable() -> Result<()> { Ok(()) } -/// Tests that a message from Thunderbird with an Autocrypt header is assigned to the PGP-contact. +/// Tests that a message from Thunderbird with an Autocrypt header is assigned to the key-contact. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_thunderbird_autocrypt() -> Result<()> { let t = TestContext::new_bob().await; @@ -3266,7 +3266,7 @@ async fn test_thunderbird_autocrypt() -> Result<()> { let from_id = message.from_id; let from_contact = Contact::get_by_id(&t, from_id).await?; - assert!(from_contact.is_pgp_contact()); + assert!(from_contact.is_key_contact()); Ok(()) } @@ -3280,7 +3280,7 @@ async fn test_prefer_encrypt_mutual_if_encrypted() -> Result<()> { // // Autocrypt header is used to check the signature. // - // At the time of the writing (2025-04-30, introduction of PGP-contacts) + // At the time of the writing (2025-04-30, introduction of key-contacts) // signature checking does not work without the Autocrypt header. let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_signed_with_pubkey.eml"); @@ -3296,7 +3296,7 @@ async fn test_prefer_encrypt_mutual_if_encrypted() -> Result<()> { let alice_id = message.from_id; let alice_contact = Contact::get_by_id(&t, alice_id).await?; - assert!(alice_contact.is_pgp_contact()); + assert!(alice_contact.is_key_contact()); // The message without the Autocrypt header // cannot be assigned to the contact even if it @@ -3318,7 +3318,7 @@ async fn test_prefer_encrypt_mutual_if_encrypted() -> Result<()> { let alice_email_id = message.from_id; assert_ne!(alice_email_id, alice_id); let alice_email_contact = Contact::get_by_id(&t, alice_email_id).await?; - assert!(!alice_email_contact.is_pgp_contact()); + assert!(!alice_email_contact.is_key_contact()); Ok(()) } @@ -3374,7 +3374,7 @@ async fn test_thunderbird_autocrypt_unencrypted() -> Result<()> { // The message should arrive as email-contact let alice_id = msg.from_id; let alice_contact = Contact::get_by_id(bob, alice_id).await?; - assert!(!alice_contact.is_pgp_contact()); + assert!(!alice_contact.is_key_contact()); let raw = include_bytes!("../../test-data/message/thunderbird_signed_unencrypted.eml"); let received_msg = receive_imf(bob, raw, false).await?.unwrap(); @@ -3386,7 +3386,7 @@ async fn test_thunderbird_autocrypt_unencrypted() -> Result<()> { let alice_id = msg.from_id; let alice_contact = Contact::get_by_id(bob, alice_id).await?; - assert!(!alice_contact.is_pgp_contact()); + assert!(!alice_contact.is_key_contact()); Ok(()) } @@ -5056,7 +5056,7 @@ PGh0bWw+PGJvZHk+dGV4dDwvYm9keT5kYXRh } /// Tests that email contacts are not added into a group -/// with PGP-contacts by a plaintext reply. +/// with key-contacts by a plaintext reply. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_no_email_contact_added_into_group() -> Result<()> { let mut tcm = TestContextManager::new(); @@ -5120,7 +5120,7 @@ async fn test_no_email_contact_added_into_group() -> Result<()> { /// if the message has a `Chat-Group-ID` even /// if there are only two members in a group. /// -/// Since PGP-contacts introduction all groups are encrypted, +/// Since key-contacts introduction all groups are encrypted, /// but old versions running on other devices might still /// create unencrypted groups. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -5151,14 +5151,14 @@ async fn test_outgoing_plaintext_two_member_group() -> Result<()> { } /// Tests that large messages are assigned -/// to non-PGP contacts if the type is not `multipart/encrypted`. +/// to non-key-contacts if the type is not `multipart/encrypted`. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_partial_download_pgp_contact_lookup() -> Result<()> { +async fn test_partial_download_key_contact_lookup() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = &tcm.alice().await; let bob = &tcm.bob().await; - // Create two chats with Alice, both with PGP-contact and email address contact. + // Create two chats with Alice, both with key-contact and email address contact. let encrypted_chat = bob.create_chat(alice).await; let unencrypted_chat = bob.create_email_chat(alice).await; diff --git a/src/securejoin.rs b/src/securejoin.rs index 237f0a811b..578138b696 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -534,7 +534,7 @@ pub(crate) async fn observe_securejoin_on_other_device( }; let Some(contact_fingerprint) = contact.fingerprint() else { - // Not a PGP-contact, should not happen. + // Not a key-contact, should not happen. warn!(context, "Contact does not have a fingerprint."); return Ok(HandshakeMessage::Ignore); }; diff --git a/src/securejoin/securejoin_tests.rs b/src/securejoin/securejoin_tests.rs index 101c9e5d52..357fa07bf2 100644 --- a/src/securejoin/securejoin_tests.rs +++ b/src/securejoin/securejoin_tests.rs @@ -211,7 +211,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) { // Alice should not yet have Bob verified let contact_bob = alice.add_or_lookup_contact_no_key(&bob).await; let contact_bob_id = contact_bob.id; - assert_eq!(contact_bob.is_pgp_contact(), true); + assert_eq!(contact_bob.is_key_contact(), true); assert_eq!(contact_bob.is_verified(&alice).await.unwrap(), false); assert_eq!(contact_bob.get_authname(), ""); diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index cb1463706f..1c9e594fc0 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1238,11 +1238,11 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint); inc_and_check(&mut migration_version, 132)?; if dbversion < migration_version { let start = Instant::now(); - sql.execute_migration_transaction(|t| migrate_pgp_contacts(context, t), migration_version) + sql.execute_migration_transaction(|t| migrate_key_contacts(context, t), migration_version) .await?; info!( context, - "PGP contacts migration took {:?} in total.", + "key-contacts migration took {:?} in total.", start.elapsed() ); } @@ -1264,11 +1264,11 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint); Ok((update_icons, disable_server_delete, recode_avatar)) } -fn migrate_pgp_contacts( +fn migrate_key_contacts( context: &Context, transaction: &mut rusqlite::Transaction<'_>, ) -> std::result::Result<(), anyhow::Error> { - info!(context, "Starting PGP contact transition."); + info!(context, "Starting key-contact transition."); // =============================== Step 1: =============================== // Alter tables @@ -1304,7 +1304,7 @@ fn migrate_pgp_contacts( SELECT secondary_verified_key_fingerprint, secondary_verified_key FROM acpeerstates WHERE secondary_verified_key_fingerprint IS NOT NULL AND secondary_verified_key IS NOT NULL;", ) - .context("Creating PGP-contact tables")?; + .context("Creating key-contact tables")?; let Some(self_addr): Option = transaction .query_row( @@ -1317,7 +1317,7 @@ fn migrate_pgp_contacts( else { info!( context, - "Not yet configured, no need to migrate PGP contacts" + "Not yet configured, no need to migrate key-contacts" ); return Ok(()); }; @@ -1327,14 +1327,14 @@ fn migrate_pgp_contacts( // one from the Autocrypt key fingerprint, one from the verified key fingerprint, // one from the secondary verified key fingerprint. // In the process, build maps from old contact id to new contact id: - // one that maps to Autocrypt PGP-contact, one that maps to verified PGP-contact. - let mut autocrypt_pgp_contacts: BTreeMap = BTreeMap::new(); - let mut autocrypt_pgp_contacts_with_reset_peerstate: BTreeMap = BTreeMap::new(); - let mut verified_pgp_contacts: BTreeMap = BTreeMap::new(); + // one that maps to Autocrypt key-contact, one that maps to verified key-contact. + let mut autocrypt_key_contacts: BTreeMap = BTreeMap::new(); + let mut autocrypt_key_contacts_with_reset_peerstate: BTreeMap = BTreeMap::new(); + let mut verified_key_contacts: BTreeMap = BTreeMap::new(); { // This maps from the verified contact to the original contact id of the verifier. - // It can't map to the verified pgp contact id, because at the time of constructing - // this map, not all pgp contacts are in the database. + // It can't map to the verified key contact id, because at the time of constructing + // this map, not all key-contacts are in the database. let mut verifications: BTreeMap = BTreeMap::new(); let mut load_contacts_stmt = transaction @@ -1490,9 +1490,9 @@ fn migrate_pgp_contacts( // prefer_encrypt == 20 would mean EncryptPreference::Reset, // i.e. we shouldn't encrypt if possible. if prefer_encrypt != 20 { - autocrypt_pgp_contacts.insert(original_id.try_into().context("Step 11")?, new_id); + autocrypt_key_contacts.insert(original_id.try_into().context("Step 11")?, new_id); } else { - autocrypt_pgp_contacts_with_reset_peerstate + autocrypt_key_contacts_with_reset_peerstate .insert(original_id.try_into().context("Step 12")?, new_id); } @@ -1500,7 +1500,7 @@ fn migrate_pgp_contacts( continue; }; let new_id = insert_contact(verified_key).context("Step 13")?; - verified_pgp_contacts.insert(original_id.try_into().context("Step 14")?, new_id); + verified_key_contacts.insert(original_id.try_into().context("Step 14")?, new_id); // If the original verifier is unknown, we represent this in the database // by putting `new_id` into the place of the verifier, // i.e. we say that this contact verified itself. @@ -1519,12 +1519,12 @@ fn migrate_pgp_contacts( } info!( context, - "Created PGP contacts identified by autocrypt key: {autocrypt_pgp_contacts:?}" + "Created key-contacts identified by autocrypt key: {autocrypt_key_contacts:?}" ); - info!(context, "Created PGP contacts with 'reset' peerstate identified by autocrypt key: {autocrypt_pgp_contacts_with_reset_peerstate:?}"); + info!(context, "Created key-contacts with 'reset' peerstate identified by autocrypt key: {autocrypt_key_contacts_with_reset_peerstate:?}"); info!( context, - "Created PGP contacts identified by verified key: {verified_pgp_contacts:?}" + "Created key-contacts identified by verified key: {verified_key_contacts:?}" ); for (&new_contact, &verifier_original_contact) in &verifications { @@ -1535,10 +1535,10 @@ fn migrate_pgp_contacts( } else { // `verifications` contains the original contact id. // We need to get the new, verified-pgp-identified contact id. - match verified_pgp_contacts.get(&verifier_original_contact) { + match verified_key_contacts.get(&verifier_original_contact) { Some(v) => *v, None => { - warn!(context, "Couldn't find PGP-contact for {verifier_original_contact} who verified {new_contact}"); + warn!(context, "Couldn't find key-contact for {verifier_original_contact} who verified {new_contact}"); continue; } } @@ -1596,18 +1596,18 @@ fn migrate_pgp_contacts( .and_then(|s| s.parse::().ok()) .unwrap_or_default() != 0; - let map_to_pgp_contact = |old_member: &u32| { + let map_to_key_contact = |old_member: &u32| { ( *old_member, - autocrypt_pgp_contacts + autocrypt_key_contacts .get(old_member) .or_else(|| { // For chatmail servers, // we send encrypted even if the peerstate is reset, // because an unencrypted message likely won't arrive. - // This is the same behavior as before PGP-contacts migration. + // This is the same behavior as before key-contacts migration. if is_chatmail { - autocrypt_pgp_contacts_with_reset_peerstate.get(old_member) + autocrypt_key_contacts_with_reset_peerstate.get(old_member) } else { None } @@ -1638,10 +1638,10 @@ fn migrate_pgp_contacts( let old_and_new_members: Vec<(u32, Option)> = match typ { // 1:1 chats retain: // - email-contact if peerstate is in the "reset" state, - // or if there is no PGP-contact that has the right email address. - // - PGP-contact identified by the Autocrypt key if Autocrypt key does not match the verified key. - // - PGP-contact identified by the verified key if peerstate Autocrypt key matches the Verified key. - // Since the autocrypt and verified PGP contact are identital in this case, we can add the AutocryptPgp contact, + // or if there is no key-contact that has the right email address. + // - key-contact identified by the Autocrypt key if Autocrypt key does not match the verified key. + // - key-contact identified by the verified key if peerstate Autocrypt key matches the Verified key. + // Since the autocrypt and verified key-contact are identital in this case, we can add the Autocrypt key-contact, // and the effect will be the same. 100 => { let Some(old_member) = old_members.first() else { @@ -1649,7 +1649,7 @@ fn migrate_pgp_contacts( continue; }; - let (_, Some(new_contact)) = map_to_pgp_contact(old_member) else { + let (_, Some(new_contact)) = map_to_key_contact(old_member) else { keep_email_contacts("No peerstate, or peerstate in 'reset' state"); continue; }; @@ -1662,7 +1662,7 @@ fn migrate_pgp_contacts( transaction .execute("UPDATE chats SET protected=0 WHERE id=?", (chat_id,))?; - keep_email_contacts("PGP contact has different email"); + keep_email_contacts("key contact has different email"); continue; } vec![(*old_member, Some(new_contact))] @@ -1679,13 +1679,13 @@ fn migrate_pgp_contacts( old_members .iter() .map(|old_member| { - (*old_member, verified_pgp_contacts.get(old_member).copied()) + (*old_member, verified_key_contacts.get(old_member).copied()) }) .collect() } else { old_members .iter() - .map(map_to_pgp_contact) + .map(map_to_key_contact) .collect::)>>() } } @@ -1702,13 +1702,13 @@ fn migrate_pgp_contacts( .map(|original| { ( *original, - autocrypt_pgp_contacts + autocrypt_key_contacts .get(original) // There will be no unencrypted broadcast lists anymore, // so, if a peerstate is reset, // the best we can do is encrypting to this key regardless. .or_else(|| { - autocrypt_pgp_contacts_with_reset_peerstate.get(original) + autocrypt_key_contacts_with_reset_peerstate.get(original) }) .copied(), ) @@ -1737,7 +1737,7 @@ fn migrate_pgp_contacts( .join(" "); info!( context, - "Migrating chat {chat_id} to PGP contacts: {human_readable_transitions}" + "Migrating chat {chat_id} to key-contacts: {human_readable_transitions}" ); for (old_member, new_member) in old_and_new_members { @@ -1778,7 +1778,7 @@ fn migrate_pgp_contacts( update_member_stmt.execute((new_member, old_member, chat_id))?; } } else { - info!(context, "Old member {old_member} in chat {chat_id} can't be upgraded to PGP-contact, removing them"); + info!(context, "Old member {old_member} in chat {chat_id} can't be upgraded to key-contact, removing them"); transaction .execute( "DELETE FROM chats_contacts WHERE contact_id=? AND chat_id=?", @@ -1843,14 +1843,14 @@ fn migrate_pgp_contacts( for msg in encrypted_msgs { let msg = msg.context("Step 34")?; - let new_from_id = *autocrypt_pgp_contacts + let new_from_id = *autocrypt_key_contacts .get(&msg.from_id) - .or_else(|| autocrypt_pgp_contacts_with_reset_peerstate.get(&msg.from_id)) + .or_else(|| autocrypt_key_contacts_with_reset_peerstate.get(&msg.from_id)) .unwrap_or(&msg.from_id); - let new_to_id = *autocrypt_pgp_contacts + let new_to_id = *autocrypt_key_contacts .get(&msg.to_id) - .or_else(|| autocrypt_pgp_contacts_with_reset_peerstate.get(&msg.to_id)) + .or_else(|| autocrypt_key_contacts_with_reset_peerstate.get(&msg.to_id)) .unwrap_or(&msg.to_id); rewrite_msg_stmt @@ -1859,7 +1859,7 @@ fn migrate_pgp_contacts( } info!( context, - "Rewriting msgs to PGP contacts took {:?}.", + "Rewriting msgs to key-contacts took {:?}.", start.elapsed() ); } diff --git a/src/sql/migrations/migrations_tests.rs b/src/sql/migrations/migrations_tests.rs index 026758e10d..c0029673d3 100644 --- a/src/sql/migrations/migrations_tests.rs +++ b/src/sql/migrations/migrations_tests.rs @@ -29,7 +29,7 @@ async fn test_clear_config_cache() -> anyhow::Result<()> { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_pgp_contacts_migration_autocrypt() -> Result<()> { +async fn test_key_contacts_migration_autocrypt() -> Result<()> { let t = STOP_MIGRATIONS_AT .scope(131, async move { TestContext::new_alice().await }) .await; @@ -67,7 +67,7 @@ async fn test_pgp_contacts_migration_autocrypt() -> Result<()> { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_pgp_contacts_migration_email1() -> Result<()> { +async fn test_key_contacts_migration_email1() -> Result<()> { let t = STOP_MIGRATIONS_AT .scope(131, async move { TestContext::new_alice().await }) .await; @@ -97,7 +97,7 @@ async fn test_pgp_contacts_migration_email1() -> Result<()> { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_pgp_contacts_migration_email2() -> Result<()> { +async fn test_key_contacts_migration_email2() -> Result<()> { let t = STOP_MIGRATIONS_AT .scope(131, async move { TestContext::new_alice().await }) .await; @@ -126,7 +126,7 @@ async fn test_pgp_contacts_migration_email2() -> Result<()> { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_pgp_contacts_migration_verified() -> Result<()> { +async fn test_key_contacts_migration_verified() -> Result<()> { let t = STOP_MIGRATIONS_AT .scope(131, async move { TestContext::new_alice().await }) .await; diff --git a/src/test_utils.rs b/src/test_utils.rs index 180dd2b221..012cecc775 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -743,7 +743,7 @@ impl TestContext { pub async fn add_or_lookup_email_contact(&self, other: &TestContext) -> Contact { let contact_id = self.add_or_lookup_email_contact_id(other).await; let contact = Contact::get_by_id(&self.ctx, contact_id).await.unwrap(); - debug_assert_eq!(contact.is_pgp_contact(), false); + debug_assert_eq!(contact.is_key_contact(), false); contact } @@ -801,11 +801,11 @@ impl TestContext { Chat::load_from_db(&self.ctx, chat_id).await.unwrap() } - /// Returns 1:1 [`Chat`] with another account PGP-contact. + /// Returns 1:1 [`Chat`] with another account key-contact. /// Panics if the chat does not exist. /// /// This first creates a contact, but does not import the key, - /// so may create a PGP-contact with a fingerprint + /// so may create a key-contact with a fingerprint /// but without the key. pub async fn get_chat(&self, other: &TestContext) -> Chat { let contact = self.add_or_lookup_contact_id(other).await; @@ -953,8 +953,8 @@ impl TestContext { "device-talk".to_string() } else if sel_chat.get_type() == Chattype::Single && !members.is_empty() { let contact = Contact::get_by_id(self, members[0]).await.unwrap(); - if contact.is_pgp_contact() { - format!("PGP {}", contact.get_addr()) + if contact.is_key_contact() { + format!("KEY {}", contact.get_addr()) } else { contact.get_addr().to_string() } diff --git a/src/tests/aeap.rs b/src/tests/aeap.rs index 526dce65b1..b3a5fcc826 100644 --- a/src/tests/aeap.rs +++ b/src/tests/aeap.rs @@ -1,9 +1,9 @@ //! "AEAP" means "Automatic Email Address Porting" -//! and was the predecessor of PGP-contacts -//! (i.e. identifying contacts via the PGP fingerprint, +//! and was the predecessor of key-contacts +//! (i.e. identifying contacts via the fingerprint, //! while allowing the email address to change). //! -//! These tests still pass because PGP-contacts +//! These tests still pass because key-contacts //! allows messaging to continue after an email address change, //! just as AEAP did. Some other tests had to be removed. @@ -236,7 +236,7 @@ async fn test_aeap_replay_attack() -> Result<()> { /// after address change. /// /// This test is redundant after introduction -/// of PGP-contacts, but is kept to avoid deleting the tests. +/// of key-contacts, but is kept to avoid deleting the tests. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_write_to_alice_after_aeap() -> Result<()> { let mut tcm = TestContextManager::new(); From 171650509c46e43348e0f5caef243ea8910b0c32 Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Thu, 26 Jun 2025 12:26:20 +0200 Subject: [PATCH 377/381] update golden tests --- src/chat/chat_tests.rs | 2 +- test-data/golden/receive_imf_older_message_from_2nd_device | 2 +- test-data/golden/test_old_message_5 | 2 +- test-data/golden/test_outgoing_encrypted_msg | 2 +- test-data/golden/test_outgoing_mua_msg | 2 +- test-data/golden/test_outgoing_mua_msg_pgp | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index dac48ec7a2..dd31a84330 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -37,7 +37,7 @@ async fn test_chat_info() { // We need to do it like this so that the test passes on Windows: serde_json::to_string( t.get_blobdir() - .join("9a17b32ad5ff71df91f7cfda9a62bb2.png") + .join("4138c52e5bc1c576cda7dd44d088c07.png") .to_str() .unwrap() ) diff --git a/test-data/golden/receive_imf_older_message_from_2nd_device b/test-data/golden/receive_imf_older_message_from_2nd_device index 993c99f9f5..abc38ad339 100644 --- a/test-data/golden/receive_imf_older_message_from_2nd_device +++ b/test-data/golden/receive_imf_older_message_from_2nd_device @@ -1,4 +1,4 @@ -Single#Chat#10: bob@example.net [bob@example.net] Icon: 9a17b32ad5ff71df91f7cfda9a62bb2.png +Single#Chat#10: bob@example.net [bob@example.net] Icon: 4138c52e5bc1c576cda7dd44d088c07.png -------------------------------------------------------------------------------- Msg#10: Me (Contact#Contact#Self): We share this account √ Msg#11: Me (Contact#Contact#Self): I'm Alice too √ diff --git a/test-data/golden/test_old_message_5 b/test-data/golden/test_old_message_5 index 473f0cdc49..624838a43c 100644 --- a/test-data/golden/test_old_message_5 +++ b/test-data/golden/test_old_message_5 @@ -1,4 +1,4 @@ -Single#Chat#10: Bob [bob@example.net] Icon: 9a17b32ad5ff71df91f7cfda9a62bb2.png +Single#Chat#10: Bob [bob@example.net] Icon: 4138c52e5bc1c576cda7dd44d088c07.png -------------------------------------------------------------------------------- Msg#10: Me (Contact#Contact#Self): Happy birthday, Bob! √ Msg#11: (Contact#Contact#10): Happy birthday to me, Alice! [FRESH] diff --git a/test-data/golden/test_outgoing_encrypted_msg b/test-data/golden/test_outgoing_encrypted_msg index d0c6e09a6e..cd3b205beb 100644 --- a/test-data/golden/test_outgoing_encrypted_msg +++ b/test-data/golden/test_outgoing_encrypted_msg @@ -1,4 +1,4 @@ -Single#Chat#10: bob@example.net [PGP bob@example.net] 🛡️ +Single#Chat#10: bob@example.net [KEY bob@example.net] 🛡️ -------------------------------------------------------------------------------- Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️] Msg#11🔒: Me (Contact#Contact#Self): Test – This is encrypted, signed, and has an Autocrypt Header without prefer-encrypt=mutual. √ diff --git a/test-data/golden/test_outgoing_mua_msg b/test-data/golden/test_outgoing_mua_msg index a96d9f9b30..5d4a0f2ee9 100644 --- a/test-data/golden/test_outgoing_mua_msg +++ b/test-data/golden/test_outgoing_mua_msg @@ -1,4 +1,4 @@ -Single#Chat#11: bob@example.net [bob@example.net] Icon: 9a17b32ad5ff71df91f7cfda9a62bb2.png +Single#Chat#11: bob@example.net [bob@example.net] Icon: 4138c52e5bc1c576cda7dd44d088c07.png -------------------------------------------------------------------------------- Msg#12: Me (Contact#Contact#Self): One classical MUA message √ -------------------------------------------------------------------------------- diff --git a/test-data/golden/test_outgoing_mua_msg_pgp b/test-data/golden/test_outgoing_mua_msg_pgp index dca2e58e5a..b302781356 100644 --- a/test-data/golden/test_outgoing_mua_msg_pgp +++ b/test-data/golden/test_outgoing_mua_msg_pgp @@ -1,4 +1,4 @@ -Single#Chat#10: bob@example.net [PGP bob@example.net] 🛡️ +Single#Chat#10: bob@example.net [KEY bob@example.net] 🛡️ -------------------------------------------------------------------------------- Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️] Msg#11🔒: (Contact#Contact#10): Heyho from DC [FRESH] From 55e6835cb1f1485c4916590fbafc7ec73dd7991e Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Thu, 26 Jun 2025 12:31:09 +0200 Subject: [PATCH 378/381] reword email_contacts to address_contacts --- ...l-contact.png => icon-address-contact.png} | Bin ...l-contact.svg => icon-address-contact.svg} | 0 deltachat-jsonrpc/src/api/types/chat.rs | 4 +-- deltachat-jsonrpc/src/api/types/chat_list.rs | 2 +- deltachat-rpc-client/tests/test_something.py | 2 +- python/tests/test_3_offline.py | 2 +- src/chat.rs | 12 ++++----- src/chat/chat_tests.rs | 24 +++++++++--------- src/contact.rs | 8 +++--- src/contact/contact_tests.rs | 6 ++--- src/receive_imf/receive_imf_tests.rs | 10 ++++---- src/sql/migrations.rs | 20 +++++++-------- src/test_utils.rs | 2 +- 13 files changed, 46 insertions(+), 46 deletions(-) rename assets/{icon-email-contact.png => icon-address-contact.png} (100%) rename assets/{icon-email-contact.svg => icon-address-contact.svg} (100%) diff --git a/assets/icon-email-contact.png b/assets/icon-address-contact.png similarity index 100% rename from assets/icon-email-contact.png rename to assets/icon-address-contact.png diff --git a/assets/icon-email-contact.svg b/assets/icon-address-contact.svg similarity index 100% rename from assets/icon-email-contact.svg rename to assets/icon-address-contact.svg diff --git a/deltachat-jsonrpc/src/api/types/chat.rs b/deltachat-jsonrpc/src/api/types/chat.rs index 77f2f1f5b3..71580b9a18 100644 --- a/deltachat-jsonrpc/src/api/types/chat.rs +++ b/deltachat-jsonrpc/src/api/types/chat.rs @@ -37,7 +37,7 @@ pub struct FullChat { /// /// False if the chat is unencrypted. /// This means that all messages in the chat are unencrypted, - /// and all contacts in the chat are "email-contacts", + /// and all contacts in the chat are "address-contacts", /// i.e. identified by the email address. /// The UI should mark this chat e.g. with a mail-letter icon. /// @@ -191,7 +191,7 @@ pub struct BasicChat { /// /// False if the chat is unencrypted. /// This means that all messages in the chat are unencrypted, - /// and all contacts in the chat are "email-contacts", + /// and all contacts in the chat are "address-contacts", /// i.e. identified by the email address. /// The UI should mark this chat e.g. with a mail-letter icon. /// diff --git a/deltachat-jsonrpc/src/api/types/chat_list.rs b/deltachat-jsonrpc/src/api/types/chat_list.rs index 3e2ae28413..c2e41c9f54 100644 --- a/deltachat-jsonrpc/src/api/types/chat_list.rs +++ b/deltachat-jsonrpc/src/api/types/chat_list.rs @@ -38,7 +38,7 @@ pub enum ChatListItemFetchResult { /// /// False if the chat is unencrypted. /// This means that all messages in the chat are unencrypted, - /// and all contacts in the chat are "email-contacts", + /// and all contacts in the chat are "address-contacts", /// i.e. identified by the email address. /// The UI should mark this chat e.g. with a mail-letter icon. /// diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index b87db8ff2e..b7b2b683c4 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -170,7 +170,7 @@ def test_account(acfactory) -> None: assert alice.get_size() assert alice.is_configured() assert not alice.get_avatar() - assert alice.get_contact_by_addr(bob_addr) is None # There is no email-contact, only key-contact + assert alice.get_contact_by_addr(bob_addr) is None # There is no address-contact, only key-contact assert alice.get_contacts() assert alice.get_contacts(snapshot=True) assert alice.self_contact diff --git a/python/tests/test_3_offline.py b/python/tests/test_3_offline.py index d991dde347..6a0a5ac40a 100644 --- a/python/tests/test_3_offline.py +++ b/python/tests/test_3_offline.py @@ -161,7 +161,7 @@ def test_create_chat_flexibility(self, acfactory): ac1 = acfactory.get_pseudo_configured_account() ac2 = acfactory.get_pseudo_configured_account() chat1 = ac1.create_chat(ac2) # This creates a key-contact chat - chat2 = ac1.create_chat(ac2.get_self_contact().addr) # This creates email-contact chat + chat2 = ac1.create_chat(ac2.get_self_contact().addr) # This creates address-contact chat assert chat1 != chat2 ac3 = acfactory.get_unconfigured_account() with pytest.raises(ValueError): diff --git a/src/chat.rs b/src/chat.rs index e713e46553..d8ed7073c4 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1748,10 +1748,10 @@ impl Chat { return contact.get_profile_image(context).await; } } else if !self.is_encrypted(context).await? { - // This is an email-contact chat, show a special avatar that marks it as such + // This is an address-contact chat, show a special avatar that marks it as such return Ok(Some(get_abs_path( context, - Path::new(&get_email_contact_icon(context).await?), + Path::new(&get_address_contact_icon(context).await?), ))); } else if let Some(image_rel) = self.param.get(Param::ProfileImage) { // Load the group avatar, or the device-chat / saved-messages icon @@ -2483,11 +2483,11 @@ pub(crate) async fn get_archive_icon(context: &Context) -> Result { .await } -pub(crate) async fn get_email_contact_icon(context: &Context) -> Result { +pub(crate) async fn get_address_contact_icon(context: &Context) -> Result { get_asset_icon( context, - "icon-email-contact", - include_bytes!("../assets/icon-email-contact.png"), + "icon-address-contact", + include_bytes!("../assets/icon-address-contact.png"), ) .await } @@ -4781,7 +4781,7 @@ async fn set_contacts_by_addrs(context: &Context, id: ChatId, addrs: &[String]) let chat = Chat::load_from_db(context, id).await?; ensure!( !chat.is_encrypted(context).await?, - "Cannot add email-contacts to encrypted chat {id}" + "Cannot add address-contacts to encrypted chat {id}" ); ensure!( chat.typ == Chattype::Broadcast, diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index dd31a84330..f82838711d 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -4254,23 +4254,23 @@ async fn test_oneone_gossip() -> Result<()> { Ok(()) } -/// Tests that email contacts cannot be added to encrypted group chats. +/// Tests that address-contacts cannot be added to encrypted group chats. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_no_email_contacts_in_group_chats() -> Result<()> { +async fn test_no_address_contacts_in_group_chats() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = &tcm.alice().await; let bob = &tcm.bob().await; let charlie = &tcm.charlie().await; let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?; - let pgp_bob_contact_id = alice.add_or_lookup_contact_id(bob).await; - let email_charlie_contact_id = alice.add_or_lookup_email_contact_id(charlie).await; + let bob_key_contact_id = alice.add_or_lookup_contact_id(bob).await; + let charlie_address_contact_id = alice.add_or_lookup_email_contact_id(charlie).await; // key-contact should be added successfully. - add_contact_to_chat(alice, chat_id, pgp_bob_contact_id).await?; + add_contact_to_chat(alice, chat_id, bob_key_contact_id).await?; - // Adding email-contact should fail. - let res = add_contact_to_chat(alice, chat_id, email_charlie_contact_id).await; + // Adding address-contact should fail. + let res = add_contact_to_chat(alice, chat_id, charlie_address_contact_id).await; assert!(res.is_err()); Ok(()) @@ -4299,14 +4299,14 @@ async fn test_no_key_contacts_in_adhoc_chats() -> Result<()> { .unwrap() .chat_id; - let email_bob_contact_id = alice.add_or_lookup_email_contact_id(bob).await; - let pgp_charlie_contact_id = alice.add_or_lookup_contact_id(charlie).await; + let bob_address_contact_id = alice.add_or_lookup_email_contact_id(bob).await; + let charlie_key_contact_id = alice.add_or_lookup_contact_id(charlie).await; - // Email-contact should be added successfully. - add_contact_to_chat(alice, chat_id, email_bob_contact_id).await?; + // Address-contact should be added successfully. + add_contact_to_chat(alice, chat_id, bob_address_contact_id).await?; // Adding key-contact should fail. - let res = add_contact_to_chat(alice, chat_id, pgp_charlie_contact_id).await; + let res = add_contact_to_chat(alice, chat_id, charlie_key_contact_id).await; assert!(res.is_err()); Ok(()) diff --git a/src/contact.rs b/src/contact.rs index f3950dfe9e..e702f7b2a2 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -820,7 +820,7 @@ impl Contact { /// Lookup a contact and create it if it does not exist yet. /// If `fingerprint` is non-empty, a key-contact with this fingerprint is added / looked up. - /// Otherwise, an email-contact with `addr` is added / looked up. + /// Otherwise, an address-contact with `addr` is added / looked up. /// A name and an "origin" can be given. /// /// The "origin" is where the address comes from - @@ -1385,14 +1385,14 @@ impl Contact { } /// Returns true if the contact is a key-contact. - /// Otherwise it is an email contact. + /// Otherwise it is an addresss-contact. pub fn is_key_contact(&self) -> bool { self.fingerprint.is_some() } /// Returns OpenPGP fingerprint of a contact. /// - /// `None` for e-mail contacts. + /// `None` for address-contacts. pub fn fingerprint(&self) -> Option { if let Some(fingerprint) = &self.fingerprint { fingerprint.parse().ok() @@ -1521,7 +1521,7 @@ impl Contact { return Ok(Some(chat::get_device_icon(context).await?)); } if show_fallback_icon && !self.id.is_special() && !self.is_key_contact() { - return Ok(Some(chat::get_email_contact_icon(context).await?)); + return Ok(Some(chat::get_address_contact_icon(context).await?)); } if let Some(image_rel) = self.param.get(Param::ProfileImage) { if !image_rel.is_empty() { diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs index 51c0b68eb3..124d767be4 100644 --- a/src/contact/contact_tests.rs +++ b/src/contact/contact_tests.rs @@ -749,11 +749,11 @@ async fn test_contact_get_encrinfo() -> Result<()> { let encrinfo = Contact::get_encrinfo(alice, ContactId::DEVICE).await; assert!(encrinfo.is_err()); - let email_contact_bob_id = alice.add_or_lookup_email_contact_id(bob).await; - let encrinfo = Contact::get_encrinfo(alice, email_contact_bob_id).await?; + let address_contact_bob_id = alice.add_or_lookup_email_contact_id(bob).await; + let encrinfo = Contact::get_encrinfo(alice, address_contact_bob_id).await?; assert_eq!(encrinfo, "No encryption"); - let contact = Contact::get_by_id(alice, email_contact_bob_id).await?; + let contact = Contact::get_by_id(alice, address_contact_bob_id).await?; assert!(!contact.e2ee_avail(alice).await?); let contact_bob_id = alice.add_or_lookup_contact_id(bob).await; diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index dcdd8f13ee..30d64ef494 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -3317,8 +3317,8 @@ async fn test_prefer_encrypt_mutual_if_encrypted() -> Result<()> { let alice_email_id = message.from_id; assert_ne!(alice_email_id, alice_id); - let alice_email_contact = Contact::get_by_id(&t, alice_email_id).await?; - assert!(!alice_email_contact.is_key_contact()); + let alice_address_contact = Contact::get_by_id(&t, alice_email_id).await?; + assert!(!alice_address_contact.is_key_contact()); Ok(()) } @@ -3371,7 +3371,7 @@ async fn test_thunderbird_autocrypt_unencrypted() -> Result<()> { let msg = Message::load_from_db(bob, msg_id).await?; assert!(!msg.get_showpadlock()); - // The message should arrive as email-contact + // The message should arrive as address-contact let alice_id = msg.from_id; let alice_contact = Contact::get_by_id(bob, alice_id).await?; assert!(!alice_contact.is_key_contact()); @@ -5055,10 +5055,10 @@ PGh0bWw+PGJvZHk+dGV4dDwvYm9keT5kYXRh Ok(()) } -/// Tests that email contacts are not added into a group +/// Tests that address-contacts are not added into a group /// with key-contacts by a plaintext reply. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_no_email_contact_added_into_group() -> Result<()> { +async fn test_no_address_contact_added_into_group() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = &tcm.alice().await; let bob = &tcm.bob().await; diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 1c9e594fc0..d8fc1722ec 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1351,7 +1351,7 @@ fn migrate_key_contacts( ) .context("Step 2")?; - let all_email_contacts: rusqlite::Result> = load_contacts_stmt + let all_address_contacts: rusqlite::Result> = load_contacts_stmt .query_map((), |row| { let id: i64 = row.get(0)?; let name: String = row.get(1)?; @@ -1416,7 +1416,7 @@ fn migrate_key_contacts( .prepare("SELECT id FROM contacts WHERE addr=? AND fingerprint='' AND id>9") .context("Step 6")?; - for row in all_email_contacts? { + for row in all_address_contacts? { let ( original_id, name, @@ -1629,7 +1629,7 @@ fn migrate_key_contacts( .collect::, rusqlite::Error>>() .context("Step 26")?; - let mut keep_email_contacts = |reason: &str| { + let mut keep_address_contacts = |reason: &str| { info!(context, "Chat {chat_id} will be an unencrypted chat with contacts identified by email address: {reason}"); for m in &old_members { orphaned_contacts.remove(m); @@ -1637,7 +1637,7 @@ fn migrate_key_contacts( }; let old_and_new_members: Vec<(u32, Option)> = match typ { // 1:1 chats retain: - // - email-contact if peerstate is in the "reset" state, + // - address-contact if peerstate is in the "reset" state, // or if there is no key-contact that has the right email address. // - key-contact identified by the Autocrypt key if Autocrypt key does not match the verified key. // - key-contact identified by the verified key if peerstate Autocrypt key matches the Verified key. @@ -1650,7 +1650,7 @@ fn migrate_key_contacts( }; let (_, Some(new_contact)) = map_to_key_contact(old_member) else { - keep_email_contacts("No peerstate, or peerstate in 'reset' state"); + keep_address_contacts("No peerstate, or peerstate in 'reset' state"); continue; }; if !addr_cmp_stmt @@ -1658,11 +1658,11 @@ fn migrate_key_contacts( { // Unprotect this 1:1 chat if it was protected. // - // Otherwise we get protected chat with email-contact. + // Otherwise we get protected chat with address-contact. transaction .execute("UPDATE chats SET protected=0 WHERE id=?", (chat_id,))?; - keep_email_contacts("key contact has different email"); + keep_address_contacts("key contact has different email"); continue; } vec![(*old_member, Some(new_contact))] @@ -1673,7 +1673,7 @@ fn migrate_key_contacts( if grpid.is_empty() { // Ad-hoc group that has empty Chat-Group-ID // because it was created in response to receiving a non-chat email. - keep_email_contacts("Empty chat-Group-ID"); + keep_address_contacts("Empty chat-Group-ID"); continue; } else if protected == 1 { old_members @@ -1692,7 +1692,7 @@ fn migrate_key_contacts( // Mailinglist 140 => { - keep_email_contacts("Mailinglist"); + keep_address_contacts("Mailinglist"); continue; } @@ -1726,7 +1726,7 @@ fn migrate_key_contacts( transaction .execute("UPDATE chats SET grpid='' WHERE id=?", (chat_id,)) .context("Step 26.1")?; - keep_email_contacts("Group contains contact without peerstate"); + keep_address_contacts("Group contains contact without peerstate"); continue; } diff --git a/src/test_utils.rs b/src/test_utils.rs index 012cecc775..d8a94f9007 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -780,7 +780,7 @@ impl TestContext { Contact::get_by_id(&self.ctx, contact_id).await.unwrap() } - /// Returns 1:1 [`Chat`] with another account email contact. + /// Returns 1:1 [`Chat`] with another account address-contact. /// Panics if it doesn't exist. /// May return a blocked chat. /// From 733675efb8ec3def8073b6f735db29238060985a Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Thu, 26 Jun 2025 13:23:47 +0200 Subject: [PATCH 379/381] stay specific in the docs --- deltachat-jsonrpc/src/api/types/chat.rs | 4 ++-- deltachat-jsonrpc/src/api/types/chat_list.rs | 2 +- src/headerdef.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/deltachat-jsonrpc/src/api/types/chat.rs b/deltachat-jsonrpc/src/api/types/chat.rs index 71580b9a18..777defe763 100644 --- a/deltachat-jsonrpc/src/api/types/chat.rs +++ b/deltachat-jsonrpc/src/api/types/chat.rs @@ -33,7 +33,7 @@ pub struct FullChat { /// True if the chat is encrypted. /// This means that all messages in the chat are encrypted, /// and all contacts in the chat are "key-contacts", - /// i.e. identified by the fingerprint. + /// i.e. identified by the PGP key fingerprint. /// /// False if the chat is unencrypted. /// This means that all messages in the chat are unencrypted, @@ -187,7 +187,7 @@ pub struct BasicChat { /// True if the chat is encrypted. /// This means that all messages in the chat are encrypted, /// and all contacts in the chat are "key-contacts", - /// i.e. identified by the fingerprint. + /// i.e. identified by the PGP key fingerprint. /// /// False if the chat is unencrypted. /// This means that all messages in the chat are unencrypted, diff --git a/deltachat-jsonrpc/src/api/types/chat_list.rs b/deltachat-jsonrpc/src/api/types/chat_list.rs index c2e41c9f54..d229d8c1f3 100644 --- a/deltachat-jsonrpc/src/api/types/chat_list.rs +++ b/deltachat-jsonrpc/src/api/types/chat_list.rs @@ -34,7 +34,7 @@ pub enum ChatListItemFetchResult { /// True if the chat is encrypted. /// This means that all messages in the chat are encrypted, /// and all contacts in the chat are "key-contacts", - /// i.e. identified by the fingerprint. + /// i.e. identified by the PGP key fingerprint. /// /// False if the chat is unencrypted. /// This means that all messages in the chat are unencrypted, diff --git a/src/headerdef.rs b/src/headerdef.rs index 91d939d575..330a4d9ba0 100644 --- a/src/headerdef.rs +++ b/src/headerdef.rs @@ -75,7 +75,7 @@ pub enum HeaderDef { /// for members listed in the `Chat-Group-Past-Members` field. ChatGroupMemberTimestamps, - /// Space-separated fingerprints + /// Space-separated PGP key fingerprints /// of group members listed in the `To` field /// followed by fingerprints /// of past members listed in the `Chat-Group-Past-Members` field. From 55ba1e8f7bee68fa0d83af7f719bd4d5d16293f0 Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Thu, 26 Jun 2025 13:52:16 +0200 Subject: [PATCH 380/381] also change to add_or_lookup_address_contact*(), even if referenced in the CHANGELOG, the api seems not to be exposed or used otherwise --- CHANGELOG.md | 4 ++-- src/chat/chat_tests.rs | 4 ++-- src/contact/contact_tests.rs | 2 +- src/receive_imf/receive_imf_tests.rs | 2 +- src/test_utils.rs | 10 +++++----- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c492f4e3e8..63af2105d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -289,8 +289,8 @@ - Use vCard in TestContext.add_or_lookup_contact(). - Remove test_group_with_removed_message_id. -- Use add_or_lookup_email_contact() in get_chat(). -- Use add_or_lookup_email_contact in test_setup_contact_ex. +- Use add_or_lookup_address_contact() in get_chat(). +- Use add_or_lookup_address_contact in test_setup_contact_ex. - Use vCards more in Python tests. - Use TestContextManager in more tests. - Use vCards to create contacts in more Rust tests. diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index f82838711d..6cf5c03681 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -4264,7 +4264,7 @@ async fn test_no_address_contacts_in_group_chats() -> Result<()> { let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?; let bob_key_contact_id = alice.add_or_lookup_contact_id(bob).await; - let charlie_address_contact_id = alice.add_or_lookup_email_contact_id(charlie).await; + let charlie_address_contact_id = alice.add_or_lookup_address_contact_id(charlie).await; // key-contact should be added successfully. add_contact_to_chat(alice, chat_id, bob_key_contact_id).await?; @@ -4299,7 +4299,7 @@ async fn test_no_key_contacts_in_adhoc_chats() -> Result<()> { .unwrap() .chat_id; - let bob_address_contact_id = alice.add_or_lookup_email_contact_id(bob).await; + let bob_address_contact_id = alice.add_or_lookup_address_contact_id(bob).await; let charlie_key_contact_id = alice.add_or_lookup_contact_id(charlie).await; // Address-contact should be added successfully. diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs index 124d767be4..ae07c65d41 100644 --- a/src/contact/contact_tests.rs +++ b/src/contact/contact_tests.rs @@ -749,7 +749,7 @@ async fn test_contact_get_encrinfo() -> Result<()> { let encrinfo = Contact::get_encrinfo(alice, ContactId::DEVICE).await; assert!(encrinfo.is_err()); - let address_contact_bob_id = alice.add_or_lookup_email_contact_id(bob).await; + let address_contact_bob_id = alice.add_or_lookup_address_contact_id(bob).await; let encrinfo = Contact::get_encrinfo(alice, address_contact_bob_id).await?; assert_eq!(encrinfo, "No encryption"); diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 30d64ef494..1555edf33d 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -2588,7 +2588,7 @@ Second thread."#; assert_eq!(bob_second_reply.chat_id, bob_second_msg.chat_id); // Alice adds Fiona to both ad hoc groups. - let alice_fiona_contact = alice.add_or_lookup_email_contact(&fiona).await; + let alice_fiona_contact = alice.add_or_lookup_address_contact(&fiona).await; let alice_fiona_contact_id = alice_fiona_contact.id; chat::add_contact_to_chat(&alice, alice_first_msg.chat_id, alice_fiona_contact_id).await?; diff --git a/src/test_utils.rs b/src/test_utils.rs index d8a94f9007..62521c33f9 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -722,7 +722,7 @@ impl TestContext { } /// Returns the [`ContactId`] for the other [`TestContext`], creating a contact if necessary. - pub async fn add_or_lookup_email_contact_id(&self, other: &TestContext) -> ContactId { + pub async fn add_or_lookup_address_contact_id(&self, other: &TestContext) -> ContactId { let primary_self_addr = other.ctx.get_primary_self_addr().await.unwrap(); let addr = ContactAddress::new(&primary_self_addr).unwrap(); // MailinglistAddress is the lowest allowed origin, we'd prefer to not modify the @@ -740,8 +740,8 @@ impl TestContext { } /// Returns the [`Contact`] for the other [`TestContext`], creating it if necessary. - pub async fn add_or_lookup_email_contact(&self, other: &TestContext) -> Contact { - let contact_id = self.add_or_lookup_email_contact_id(other).await; + pub async fn add_or_lookup_address_contact(&self, other: &TestContext) -> Contact { + let contact_id = self.add_or_lookup_address_contact_id(other).await; let contact = Contact::get_by_id(&self.ctx, contact_id).await.unwrap(); debug_assert_eq!(contact.is_key_contact(), false); contact @@ -787,7 +787,7 @@ impl TestContext { /// This first creates a contact using the configured details on the other account, then /// gets the 1:1 chat with this contact. pub async fn get_email_chat(&self, other: &TestContext) -> Chat { - let contact = self.add_or_lookup_email_contact(other).await; + let contact = self.add_or_lookup_address_contact(other).await; let chat_id = ChatIdBlocked::lookup_by_contact(&self.ctx, contact.id) .await @@ -838,7 +838,7 @@ impl TestContext { /// /// This function can be used to create unencrypted chats. pub async fn create_email_chat(&self, other: &TestContext) -> Chat { - let contact = self.add_or_lookup_email_contact(other).await; + let contact = self.add_or_lookup_address_contact(other).await; let chat_id = ChatId::create_for_contact(self, contact.id).await.unwrap(); Chat::load_from_db(self, chat_id).await.unwrap() From 877cf0f6649a122d29639c2235d3e23278eda7db Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 26 Jun 2025 13:47:49 +0000 Subject: [PATCH 381/381] encrypted chat --- src/receive_imf.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 8aa3b912fa..e0264d6056 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -391,7 +391,7 @@ async fn get_to_and_past_contact_ids( { // There is a single recipient and we have // mapped it to a key contact. - // This is a 1:1 key-chat. + // This is an encrypted 1:1 chat. to_ids = pgp_to_ids } else if let Some(chat_id) = chat_id { to_ids = lookup_key_contacts_by_address_list(