diff --git a/CHANGELOG.md b/CHANGELOG.md index d4c1e029d..329f52089 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## [1.122.0] - 2023-09-12 + +### API-Changes + +- jsonrpc: Return only chat IDs for similar chats. + +### Fixes + +- Reopen all connections on database passpharse change. +- Do not block new group chats if 1:1 chat is blocked. +- Improve group membership consistency algorithm ([#3782](https://github.com/deltachat/deltachat-core-rust/pull/3782))([#4624](https://github.com/deltachat/deltachat-core-rust/pull/4624)). +- Forbid membership changes from possible non-members ([#3782](https://github.com/deltachat/deltachat-core-rust/pull/3782)). +- `ChatId::parent_query()`: Don't filter out OutPending and OutFailed messages. + +### Build system + +- Update to OpenSSL 3.0. +- Bump webpki from 0.22.0 to 0.22.1. +- python: Add link to Mastodon into projects.urls. + +### Features / Changes + +- Add RSA-4096 key generation support. + +### Refactor + +- pgp: Add constants for encryption algorithm and hash. + ## [1.121.0] - 2023-09-06 ### API-Changes @@ -2787,3 +2815,4 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed [1.119.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.0...v1.119.1 [1.120.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.1...v1.120.0 [1.121.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.120.0...v1.121.0 +[1.122.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.121.0...v1.122.0 diff --git a/Cargo.lock b/Cargo.lock index e2008e24f..13458b6af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1085,7 +1085,7 @@ dependencies = [ [[package]] name = "deltachat" -version = "1.121.0" +version = "1.122.0" dependencies = [ "ansi_term", "anyhow", @@ -1162,7 +1162,7 @@ dependencies = [ [[package]] name = "deltachat-jsonrpc" -version = "1.121.0" +version = "1.122.0" dependencies = [ "anyhow", "async-channel", @@ -1186,7 +1186,7 @@ dependencies = [ [[package]] name = "deltachat-repl" -version = "1.121.0" +version = "1.122.0" dependencies = [ "ansi_term", "anyhow", @@ -1201,7 +1201,7 @@ dependencies = [ [[package]] name = "deltachat-rpc-server" -version = "1.121.0" +version = "1.122.0" dependencies = [ "anyhow", "deltachat", @@ -1226,7 +1226,7 @@ dependencies = [ [[package]] name = "deltachat_ffi" -version = "1.121.0" +version = "1.122.0" dependencies = [ "anyhow", "deltachat", @@ -3025,11 +3025,11 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.55" +version = "0.10.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" +checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.3.3", "cfg-if", "foreign-types", "libc", @@ -3057,18 +3057,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "111.26.0+1.1.1u" +version = "300.1.3+3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efc62c9f12b22b8f5208c23a7200a442b2e5999f8bdf80233852122b5a4f6f37" +checksum = "cd2c101a165fff9935e34def4669595ab1c7847943c42be86e21503e482be107" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.90" +version = "0.9.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" +checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" dependencies = [ "cc", "libc", diff --git a/Cargo.toml b/Cargo.toml index 4a9fea5f0..12b380bdb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat" -version = "1.121.0" +version = "1.122.0" edition = "2021" license = "MPL-2.0" rust-version = "1.67" diff --git a/deltachat-ffi/Cargo.toml b/deltachat-ffi/Cargo.toml index 5bafbc735..0611e8b79 100644 --- a/deltachat-ffi/Cargo.toml +++ b/deltachat-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat_ffi" -version = "1.121.0" +version = "1.122.0" description = "Deltachat FFI" edition = "2018" readme = "README.md" diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 78798e1e3..0d7915668 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -443,7 +443,9 @@ char* dc_get_blobdir (const dc_context_t* context); * DC_KEY_GEN_RSA2048 (1)= * generate RSA 2048 keypair * DC_KEY_GEN_ED25519 (2)= - * generate Ed25519 keypair + * generate Curve25519 keypair + * DC_KEY_GEN_RSA4096 (3)= + * generate RSA 4096 keypair * - `save_mime_headers` = 1=save mime headers * and make dc_get_mime_headers() work for subsequent calls, * 0=do not save mime headers (default) @@ -6309,6 +6311,7 @@ void dc_event_unref(dc_event_t* event); #define DC_KEY_GEN_DEFAULT 0 #define DC_KEY_GEN_RSA2048 1 #define DC_KEY_GEN_ED25519 2 +#define DC_KEY_GEN_RSA4096 3 /** diff --git a/deltachat-jsonrpc/Cargo.toml b/deltachat-jsonrpc/Cargo.toml index f1b4d15a7..6732af2ed 100644 --- a/deltachat-jsonrpc/Cargo.toml +++ b/deltachat-jsonrpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-jsonrpc" -version = "1.121.0" +version = "1.122.0" description = "DeltaChat JSON-RPC API" edition = "2021" default-run = "deltachat-jsonrpc-server" diff --git a/deltachat-jsonrpc/src/api/mod.rs b/deltachat-jsonrpc/src/api/mod.rs index c2c57ea2b..8f6293bfb 100644 --- a/deltachat-jsonrpc/src/api/mod.rs +++ b/deltachat-jsonrpc/src/api/mod.rs @@ -39,7 +39,6 @@ pub mod types; use num_traits::FromPrimitive; use types::account::Account; use types::chat::FullChat; -use types::chat_list::ChatListEntry; use types::contact::ContactObject; use types::events::Event; use types::http::HttpResponse; @@ -571,22 +570,18 @@ impl CommandApi { } /// Returns chats similar to the given one. - async fn get_similar_chatlist_entries( - &self, - account_id: u32, - chat_id: u32, - ) -> Result> { + /// + /// Experimental API, subject to change without notice. + async fn get_similar_chat_ids(&self, account_id: u32, chat_id: u32) -> Result> { let ctx = self.get_context(account_id).await?; let chat_id = ChatId::new(chat_id); - let list = chat_id.get_similar_chatlist(&ctx).await?; - let mut l: Vec = Vec::with_capacity(list.len()); - for i in 0..list.len() { - l.push(ChatListEntry( - list.get_chat_id(i)?.to_u32(), - list.get_msg_id(i)?.unwrap_or_default().to_u32(), - )); - } - Ok(l) + let list = chat_id + .get_similar_chat_ids(&ctx) + .await? + .into_iter() + .map(|(chat_id, _metric)| chat_id.to_u32()) + .collect(); + Ok(list) } async fn get_chatlist_items_by_entries( diff --git a/deltachat-jsonrpc/src/api/types/chat_list.rs b/deltachat-jsonrpc/src/api/types/chat_list.rs index 510c20e20..b47f7a728 100644 --- a/deltachat-jsonrpc/src/api/types/chat_list.rs +++ b/deltachat-jsonrpc/src/api/types/chat_list.rs @@ -8,15 +8,12 @@ use deltachat::{ chatlist::Chatlist, }; use num_traits::cast::ToPrimitive; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use typescript_type_def::TypeDef; use super::color_int_to_hex_string; use super::message::MessageViewtype; -#[derive(Deserialize, Serialize, TypeDef, schemars::JsonSchema)] -pub struct ChatListEntry(pub u32, pub u32); - #[derive(Serialize, TypeDef, schemars::JsonSchema)] #[serde(tag = "kind")] pub enum ChatListItemFetchResult { diff --git a/deltachat-jsonrpc/typescript/package.json b/deltachat-jsonrpc/typescript/package.json index 3f3fd3d16..51473e565 100644 --- a/deltachat-jsonrpc/typescript/package.json +++ b/deltachat-jsonrpc/typescript/package.json @@ -55,5 +55,5 @@ }, "type": "module", "types": "dist/deltachat.d.ts", - "version": "1.121.0" + "version": "1.122.0" } diff --git a/deltachat-repl/Cargo.toml b/deltachat-repl/Cargo.toml index 3d52b6b13..35935518a 100644 --- a/deltachat-repl/Cargo.toml +++ b/deltachat-repl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-repl" -version = "1.121.0" +version = "1.122.0" license = "MPL-2.0" edition = "2021" diff --git a/deltachat-rpc-server/Cargo.toml b/deltachat-rpc-server/Cargo.toml index 796e63b17..5a59e2a52 100644 --- a/deltachat-rpc-server/Cargo.toml +++ b/deltachat-rpc-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-rpc-server" -version = "1.121.0" +version = "1.122.0" description = "DeltaChat JSON-RPC server" edition = "2021" readme = "README.md" diff --git a/node/constants.js b/node/constants.js index ad464c81a..b9c990c77 100644 --- a/node/constants.js +++ b/node/constants.js @@ -90,6 +90,7 @@ module.exports = { DC_KEY_GEN_DEFAULT: 0, DC_KEY_GEN_ED25519: 2, DC_KEY_GEN_RSA2048: 1, + DC_KEY_GEN_RSA4096: 3, DC_LP_AUTH_NORMAL: 4, DC_LP_AUTH_OAUTH2: 2, DC_MEDIA_QUALITY_BALANCED: 0, diff --git a/node/lib/constants.ts b/node/lib/constants.ts index 76b7707f7..6cc08f144 100644 --- a/node/lib/constants.ts +++ b/node/lib/constants.ts @@ -90,6 +90,7 @@ export enum C { DC_KEY_GEN_DEFAULT = 0, DC_KEY_GEN_ED25519 = 2, DC_KEY_GEN_RSA2048 = 1, + DC_KEY_GEN_RSA4096 = 3, DC_LP_AUTH_NORMAL = 4, DC_LP_AUTH_OAUTH2 = 2, DC_MEDIA_QUALITY_BALANCED = 0, diff --git a/package.json b/package.json index 779fe9c0e..a2cd51844 100644 --- a/package.json +++ b/package.json @@ -60,5 +60,5 @@ "test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit" }, "types": "node/dist/index.d.ts", - "version": "1.121.0" + "version": "1.122.0" } diff --git a/python/pyproject.toml b/python/pyproject.toml index 024f62dc0..5e0bee124 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -34,6 +34,7 @@ dynamic = [ "Home" = "https://github.com/deltachat/deltachat-core-rust/" "Bug Tracker" = "https://github.com/deltachat/deltachat-core-rust/issues" "Documentation" = "https://py.delta.chat/" +"Mastodon" = "https://chaos.social/@delta" [project.entry-points.pytest11] "deltachat.testplugin" = "deltachat.testplugin" diff --git a/python/tests/test_1_online.py b/python/tests/test_1_online.py index 160422211..75bfe3a23 100644 --- a/python/tests/test_1_online.py +++ b/python/tests/test_1_online.py @@ -1699,6 +1699,36 @@ def test_qr_join_chat(acfactory, lp, verified_one_on_one_chats): assert not ch.is_protected() +def test_qr_new_group_unblocked(acfactory, lp): + """Regression test for a bug intoduced in core v1.113.0. + ac2 scans a verified group QR code created by ac1. + This results in creation of a blocked 1:1 chat with ac1 on ac2, + but ac1 contact is not blocked on ac2. + Then ac1 creates a group, adds ac2 there and promotes it by sending a message. + ac2 should receive a message and create a contact request for the group. + Due to a bug previously ac2 created a blocked group. + """ + + ac1, ac2 = acfactory.get_online_accounts(2) + ac1_chat = ac1.create_group_chat("Group for joining", verified=True) + qr = ac1_chat.get_join_qr() + ac2.qr_join_chat(qr) + + ac1._evtracker.wait_securejoin_inviter_progress(1000) + + ac1_new_chat = ac1.create_group_chat("Another group") + ac1_new_chat.add_contact(ac2) + ac1_new_chat.send_text("Hello!") + + # Receive "Member added" message. + ac2._evtracker.wait_next_incoming_message() + + # Receive "Hello!" message. + ac2_msg = ac2._evtracker.wait_next_incoming_message() + assert ac2_msg.text == "Hello!" + assert ac2_msg.chat.is_contact_request() + + def test_qr_email_capitalization(acfactory, lp): """Regression test for a bug that resulted in failure to propagate verification via gossip in a verified group diff --git a/release-date.in b/release-date.in index 1cef16d38..05f0fc2f9 100644 --- a/release-date.in +++ b/release-date.in @@ -1 +1 @@ -2023-09-06 \ No newline at end of file +2023-09-12 \ No newline at end of file diff --git a/src/chat.rs b/src/chat.rs index 7a9ab77c9..103702e76 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1054,7 +1054,7 @@ impl ChatId { let sql = &context.sql; let query = format!( "SELECT {fields} \ - FROM msgs WHERE chat_id=? AND state NOT IN (?, ?, ?, ?) AND NOT hidden \ + FROM msgs WHERE chat_id=? AND state NOT IN (?, ?) AND NOT hidden \ ORDER BY timestamp DESC, id DESC \ LIMIT 1;" ); @@ -1065,8 +1065,11 @@ impl ChatId { self, MessageState::OutPreparing, MessageState::OutDraft, - MessageState::OutPending, - MessageState::OutFailed, + // We don't filter `OutPending` and `OutFailed` messages because the new message + // for which `parent_query()` is done may assume that it will be received in a + // context affected by those messages, e.g. they could add new members to a + // group and the new message will contain them in "To:". Anyway recipients must + // be prepared to orphaned references. ), f, ) diff --git a/src/constants.rs b/src/constants.rs index 4adc2560e..165de5391 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -62,8 +62,15 @@ pub enum MediaQuality { pub enum KeyGenType { #[default] Default = 0, + + /// 2048-bit RSA. Rsa2048 = 1, + + /// [Ed25519](https://ed25519.cr.yp.to/) signature and X25519 encryption. Ed25519 = 2, + + /// 4096-bit RSA. + Rsa4096 = 3, } /// Video chat URL type. @@ -224,6 +231,7 @@ mod tests { assert_eq!(KeyGenType::Default, KeyGenType::from_i32(0).unwrap()); assert_eq!(KeyGenType::Rsa2048, KeyGenType::from_i32(1).unwrap()); assert_eq!(KeyGenType::Ed25519, KeyGenType::from_i32(2).unwrap()); + assert_eq!(KeyGenType::Rsa4096, KeyGenType::from_i32(3).unwrap()); } #[test] diff --git a/src/context.rs b/src/context.rs index fe29cb8e8..07304796b 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1465,6 +1465,35 @@ mod tests { Ok(()) } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_context_change_passphrase() -> Result<()> { + let dir = tempdir()?; + let dbfile = dir.path().join("db.sqlite"); + + let id = 1; + let context = Context::new_closed(&dbfile, id, Events::new(), StockStrings::new()) + .await + .context("failed to create context")?; + assert_eq!(context.open("foo".to_string()).await?, true); + assert_eq!(context.is_open().await, true); + + context + .set_config(Config::Addr, Some("alice@example.org")) + .await?; + + context + .change_passphrase("bar".to_string()) + .await + .context("Failed to change passphrase")?; + + assert_eq!( + context.get_config(Config::Addr).await?.unwrap(), + "alice@example.org" + ); + + Ok(()) + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_ongoing() -> Result<()> { let context = TestContext::new().await; diff --git a/src/pgp.rs b/src/pgp.rs index 73123074c..c6989bbb5 100644 --- a/src/pgp.rs +++ b/src/pgp.rs @@ -29,6 +29,12 @@ pub(crate) const HEADER_AUTOCRYPT: &str = "autocrypt-prefer-encrypt"; #[allow(missing_docs)] pub const HEADER_SETUPCODE: &str = "passphrase-begin"; +/// Preferred symmetric encryption algorithm. +const SYMMETRIC_KEY_ALGORITHM: SymmetricKeyAlgorithm = SymmetricKeyAlgorithm::AES128; + +/// Preferred cryptographic hash. +const HASH_ALGORITHM: HashAlgorithm = HashAlgorithm::SHA2_256; + /// A wrapper for rPGP public key types #[derive(Debug)] enum SignedPublicKeyOrSubkey<'a> { @@ -135,6 +141,7 @@ pub struct KeyPair { pub(crate) fn create_keypair(addr: EmailAddress, keygen_type: KeyGenType) -> Result { let (secret_key_type, public_key_type) = match keygen_type { KeyGenType::Rsa2048 => (PgpKeyType::Rsa(2048), PgpKeyType::Rsa(2048)), + KeyGenType::Rsa4096 => (PgpKeyType::Rsa(4096), PgpKeyType::Rsa(4096)), KeyGenType::Ed25519 | KeyGenType::Default => (PgpKeyType::EdDSA, PgpKeyType::ECDH), }; @@ -247,11 +254,13 @@ pub async fn pk_encrypt( // TODO: measure time let encrypted_msg = if let Some(ref skey) = private_key_for_signing { lit_msg - .sign(skey, || "".into(), Default::default()) + .sign(skey, || "".into(), HASH_ALGORITHM) .and_then(|msg| msg.compress(CompressionAlgorithm::ZLIB)) - .and_then(|msg| msg.encrypt_to_keys(&mut rng, Default::default(), &pkeys_refs)) + .and_then(|msg| { + msg.encrypt_to_keys(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs) + }) } else { - lit_msg.encrypt_to_keys(&mut rng, Default::default(), &pkeys_refs) + lit_msg.encrypt_to_keys(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs) }; let msg = encrypted_msg?; @@ -270,7 +279,7 @@ pub fn pk_calc_signature( let msg = Message::new_literal_bytes("", plain).sign( private_key_for_signing, || "".into(), - Default::default(), + HASH_ALGORITHM, )?; let signature = msg.into_signature().to_armored_string(None)?; Ok(signature) @@ -359,7 +368,7 @@ pub async fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result { let mut rng = thread_rng(); let s2k = StringToKey::new_default(&mut rng); let msg = - lit_msg.encrypt_with_password(&mut rng, s2k, Default::default(), || passphrase)?; + lit_msg.encrypt_with_password(&mut rng, s2k, SYMMETRIC_KEY_ALGORITHM, || passphrase)?; let encoded_msg = msg.to_armored_string(None)?; diff --git a/src/receive_imf.rs b/src/receive_imf.rs index ff961dec4..05cf0c499 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -550,19 +550,30 @@ async fn add_parts( // signals whether the current user is a bot let is_bot = context.get_config_bool(Config::Bot).await?; - let create_blocked = match test_normal_chat { - Some(ChatIdBlocked { - id: _, - blocked: Blocked::Request, - }) if is_bot => Blocked::Not, - Some(ChatIdBlocked { id: _, blocked }) => blocked, - None => { - if is_bot { - Blocked::Not - } else { - Blocked::Request + let create_blocked_default = if is_bot { + Blocked::Not + } else { + Blocked::Request + }; + let create_blocked = if let Some(ChatIdBlocked { id: _, blocked }) = test_normal_chat { + match blocked { + Blocked::Request => create_blocked_default, + Blocked::Not => Blocked::Not, + Blocked::Yes => { + if Contact::is_blocked_load(context, from_id).await? { + // User has blocked the contact. + // Block the group contact created as well. + Blocked::Yes + } else { + // 1:1 chat is blocked, but the contact is not. + // This happens when 1:1 chat is hidden + // during scanning of a group invitation code. + Blocked::Request + } } } + } else { + create_blocked_default }; if chat_id.is_none() { @@ -1701,40 +1712,37 @@ async fn apply_group_changes( false }; - // Whether to allow any changes to the member list at all. - let allow_member_list_changes = if chat::is_contact_in_chat(context, chat_id, ContactId::SELF) - .await? - || self_added - || !mime_parser.has_chat_version() - { - // Reject old group changes. - chat_id + let is_from_in_chat = !chat::is_contact_in_chat(context, chat_id, ContactId::SELF).await? + || chat::is_contact_in_chat(context, chat_id, from_id).await?; + + // Reject group membership changes from non-members and old changes. + let allow_member_list_changes = is_from_in_chat + && chat_id .update_timestamp(context, Param::MemberListTimestamp, sent_timestamp) - .await? - } else { - // Member list changes are not allowed if we're not in the group - // and are not explicitly added. - // This message comes from a Delta Chat that restored an old backup - // or the message is a MUA reply to an old message. - false - }; + .await?; // Whether to rebuild the member list from scratch. - let recreate_member_list = if allow_member_list_changes { + let recreate_member_list = { // Recreate member list if the message comes from a MUA as these messages do _not_ set add/remove headers. - // Always recreate membership list if self has been added. - if !mime_parser.has_chat_version() || self_added { - true - } else { - match mime_parser.get_header(HeaderDef::InReplyTo) { + !mime_parser.has_chat_version() + // Always recreate membership list if SELF has been added. The older versions of DC + // don't always set "In-Reply-To" to the latest message they sent, but to the latest + // delivered message (so it's a race), so we have this heuristic here. + || self_added + || match mime_parser.get_header(HeaderDef::InReplyTo) { // If we don't know the referenced message, we missed some messages. // Maybe they added/removed members, so we need to recreate our member list. Some(reply_to) => rfc724_mid_exists(context, reply_to).await?.is_none(), None => false, } + } && { + if !allow_member_list_changes { + info!( + context, + "Ignoring a try to recreate member list of {chat_id} by {from_id}.", + ); } - } else { - false + allow_member_list_changes }; if mime_parser.get_header(HeaderDef::ChatVerified).is_some() { @@ -1843,43 +1851,35 @@ async fn apply_group_changes( // Recreate the member list. if recreate_member_list { - if !chat::is_contact_in_chat(context, chat_id, from_id).await? { - warn!( - context, - "Contact {from_id} attempts to modify group chat {chat_id} member list without being a member." - ); - } else { - // Only delete old contacts if the sender is not a classical MUA user: - // Classical MUA users usually don't intend to remove users from an email - // thread, so if they removed a recipient then it was probably by accident. - if mime_parser.has_chat_version() { - context - .sql - .execute("DELETE FROM chats_contacts WHERE chat_id=?;", (chat_id,)) - .await?; - } - - let mut members_to_add = HashSet::new(); - members_to_add.extend(to_ids); - members_to_add.insert(ContactId::SELF); - - if !from_id.is_special() { - members_to_add.insert(from_id); - } - - if let Some(removed_id) = removed_id { - members_to_add.remove(&removed_id); - } - - info!( - context, - "Recreating chat {chat_id} with members {members_to_add:?}." - ); - - chat::add_to_chat_contacts_table(context, chat_id, &Vec::from_iter(members_to_add)) + // Only delete old contacts if the sender is not a classical MUA user: + // Classical MUA users usually don't intend to remove users from an email + // thread, so if they removed a recipient then it was probably by accident. + if mime_parser.has_chat_version() { + context + .sql + .execute("DELETE FROM chats_contacts WHERE chat_id=?;", (chat_id,)) .await?; - send_event_chat_modified = true; } + + let mut members_to_add = HashSet::new(); + members_to_add.extend(to_ids); + members_to_add.insert(ContactId::SELF); + + if !from_id.is_special() { + members_to_add.insert(from_id); + } + + if let Some(removed_id) = removed_id { + members_to_add.remove(&removed_id); + } + + info!( + context, + "Recreating chat {chat_id} with members {members_to_add:?}." + ); + + chat::add_to_chat_contacts_table(context, chat_id, &Vec::from_iter(members_to_add)).await?; + send_event_chat_modified = true; } if let Some(avatar_action) = &mime_parser.group_avatar { diff --git a/src/receive_imf/tests.rs b/src/receive_imf/tests.rs index 4be60ba72..616d27b52 100644 --- a/src/receive_imf/tests.rs +++ b/src/receive_imf/tests.rs @@ -8,6 +8,7 @@ use crate::chat::{ }; use crate::chat::{get_chat_msgs, ChatItem, ChatVisibility}; use crate::chatlist::Chatlist; +use crate::config::Config; use crate::constants::{DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS}; use crate::imap::prefetch_should_download; use crate::message::Message; @@ -3611,3 +3612,70 @@ async fn test_mua_can_readd() -> Result<()> { assert!(is_contact_in_chat(&alice, alice_chat.id, ContactId::SELF).await?); Ok(()) } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_recreate_member_list_on_missing_add_of_self() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?; + add_contact_to_chat( + &alice, + alice_chat_id, + Contact::create(&alice, "bob", &bob.get_config(Config::Addr).await?.unwrap()).await?, + ) + .await?; + send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?; + alice.pop_sent_msg().await; + remove_contact_from_chat(&alice, alice_chat_id, ContactId::SELF).await?; + let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id; + + // Bob missed the message adding them, but must recreate the member list. + assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 1); + assert!(is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_keep_member_list_if_possibly_nomember() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?; + add_contact_to_chat( + &alice, + alice_chat_id, + Contact::create(&alice, "bob", &bob.get_config(Config::Addr).await?.unwrap()).await?, + ) + .await?; + send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?; + let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id; + + let fiona = TestContext::new_fiona().await; + add_contact_to_chat( + &alice, + alice_chat_id, + Contact::create( + &alice, + "fiona", + &fiona.get_config(Config::Addr).await?.unwrap(), + ) + .await?, + ) + .await?; + let fiona_chat_id = fiona.recv_msg(&alice.pop_sent_msg().await).await.chat_id; + fiona_chat_id.accept(&fiona).await?; + + send_text_msg(&fiona, fiona_chat_id, "hi".to_string()).await?; + bob.recv_msg(&fiona.pop_sent_msg().await).await; + + // Bob missed the message adding fiona, but mustn't recreate the member list. + assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2); + assert!(is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?); + let bob_alice_contact = Contact::create( + &bob, + "alice", + &alice.get_config(Config::Addr).await?.unwrap(), + ) + .await?; + assert!(is_contact_in_chat(&bob, bob_chat_id, bob_alice_contact).await?); + Ok(()) +} diff --git a/src/sql.rs b/src/sql.rs index ab780a452..ceb38067e 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -310,12 +310,17 @@ impl Sql { /// It is impossible to turn encrypted database into unencrypted /// and vice versa this way, use import/export for this. pub async fn change_passphrase(&self, passphrase: String) -> Result<()> { - self.call_write(move |conn| { - conn.pragma_update(None, "rekey", passphrase) - .context("failed to set PRAGMA rekey")?; - Ok(()) - }) - .await + let mut lock = self.pool.write().await; + + let pool = lock.take().context("SQL connection pool is not open")?; + let conn = pool.get().await?; + conn.pragma_update(None, "rekey", passphrase.clone()) + .context("failed to set PRAGMA rekey")?; + drop(pool); + + *lock = Some(Self::new_pool(&self.dbfile, passphrase.to_string())?); + + Ok(()) } /// Locks the write transactions mutex in order to make sure that there never are @@ -1265,7 +1270,7 @@ mod tests { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_change_passphrase() -> Result<()> { + async fn test_sql_change_passphrase() -> Result<()> { use tempfile::tempdir; // The context is used only for logging. @@ -1289,6 +1294,23 @@ mod tests { sql.change_passphrase("bar".to_string()) .await .context("failed to change passphrase")?; + + // Test that at least two connections are still working. + // This ensures that not only the connection which changed the password is working, + // but other connections as well. + { + let lock = sql.pool.read().await; + let pool = lock.as_ref().unwrap(); + let conn1 = pool.get().await?; + let conn2 = pool.get().await?; + conn1 + .query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(())) + .unwrap(); + conn2 + .query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(())) + .unwrap(); + } + sql.close().await; let sql = Sql::new(dbfile);