diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b85b67d1e4..53a6945a29 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,6 +7,8 @@ updates: commit-message: prefix: "chore(cargo)" open-pull-requests-limit: 50 + cooldown: + default-days: 7 # Keep GitHub Actions up to date. # @@ -14,3 +16,5 @@ updates: directory: "/" schedule: interval: "weekly" + cooldown: + default-days: 7 diff --git a/.github/workflows/deltachat-rpc-server.yml b/.github/workflows/deltachat-rpc-server.yml index 46bd640f43..91936a1072 100644 --- a/.github/workflows/deltachat-rpc-server.yml +++ b/.github/workflows/deltachat-rpc-server.yml @@ -34,7 +34,7 @@ jobs: with: show-progress: false persist-credentials: false - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31 - name: Build deltachat-rpc-server binaries run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux @@ -58,7 +58,7 @@ jobs: with: show-progress: false persist-credentials: false - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31 - name: Build deltachat-rpc-server binaries run: nix build .#deltachat-rpc-server-${{ matrix.arch }} @@ -109,7 +109,7 @@ jobs: with: show-progress: false persist-credentials: false - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31 - name: Build deltachat-rpc-server binaries run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android @@ -136,7 +136,7 @@ jobs: with: show-progress: false persist-credentials: false - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31 - name: Download Linux aarch64 binary uses: actions/download-artifact@v5 diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 6dbe7fbbf0..425da3280f 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -25,7 +25,7 @@ jobs: with: show-progress: false persist-credentials: false - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31 - run: nix fmt flake.nix -- --check build: @@ -84,7 +84,7 @@ jobs: with: show-progress: false persist-credentials: false - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31 - run: nix build .#${{ matrix.installable }} build-macos: @@ -95,14 +95,15 @@ jobs: matrix: installable: - deltachat-rpc-server + - deltachat-rpc-server-x86_64-darwin - # Fails to bulid + # Fails to build + # because of . # - deltachat-rpc-server-aarch64-darwin - # - deltachat-rpc-server-x86_64-darwin steps: - uses: actions/checkout@v5 with: show-progress: false persist-credentials: false - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31 - run: nix build .#${{ matrix.installable }} diff --git a/.github/workflows/repl.yml b/.github/workflows/repl.yml index 0544428eac..ab05c67a3b 100644 --- a/.github/workflows/repl.yml +++ b/.github/workflows/repl.yml @@ -18,7 +18,7 @@ jobs: with: show-progress: false persist-credentials: false - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31 - name: Build run: nix build .#deltachat-repl-win64 - name: Upload binary diff --git a/.github/workflows/upload-docs.yml b/.github/workflows/upload-docs.yml index d9e65afefa..a6223b6c07 100644 --- a/.github/workflows/upload-docs.yml +++ b/.github/workflows/upload-docs.yml @@ -36,7 +36,7 @@ jobs: show-progress: false persist-credentials: false fetch-depth: 0 # Fetch history to calculate VCS version number. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31 - name: Build Python documentation run: nix build .#python-docs - name: Upload to py.delta.chat @@ -55,7 +55,7 @@ jobs: show-progress: false persist-credentials: false fetch-depth: 0 # Fetch history to calculate VCS version number. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31 - name: Build C documentation run: nix build .#docs - name: Upload to c.delta.chat diff --git a/.github/workflows/zizmor-scan.yml b/.github/workflows/zizmor-scan.yml index 0c0cfe516a..86f9acbe2f 100644 --- a/.github/workflows/zizmor-scan.yml +++ b/.github/workflows/zizmor-scan.yml @@ -19,13 +19,13 @@ jobs: persist-credentials: false - name: Install the latest version of uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d - name: Run zizmor run: uvx zizmor --format sarif . > results.sarif - name: Upload SARIF file - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@v4 with: sarif_file: results.sarif category: zizmor diff --git a/.gitignore b/.gitignore index 0be33f761e..78c482c5c1 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ deltachat-ffi/xml coverage/ .DS_Store .vscode +.zed python/accounts.txt python/all-testaccounts.txt tmp/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 46d6e66826..56c95063bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,60 @@ # Changelog +## [2.22.0] - 2025-10-17 + +### Fixes + +- Do not notify about incoming calls for contact requests and blocked contacts. + +### Tests + +- Accept the chat with the caller before accepting calls. + +## [2.21.0] - 2025-10-16 + +### Build system + +- nix: Remove unused dependencies. + +### Features / Changes + +- TLS 1.3 session resumption. +- REPL: Add send-sync command. +- Set `User-Agent` for tile.openstreetmap.org requests. +- Cache tile.openstreetmap.org tiles for 7 days. + +### Fixes + +- Remove Exif with non-fatal errors from images. +- jsonrpc: Use Core's logic for computing VcardContact.color ([#7294](https://github.com/chatmail/core/pull/7294)). + +### Miscellaneous Tasks + +- deps: Bump cachix/install-nix-action from 31.7.0 to 31.8.0. +- cargo: Bump async_zip from 0.0.17 to 0.0.18 ([#7257](https://github.com/chatmail/core/pull/7257)). +- deps: Bump github/codeql-action from 3 to 4 ([#7304](https://github.com/chatmail/core/pull/7304)). + +### Refactor + +- Use rustls reexported from tokio_rustls. +- Pass ALPN around as &str. +- mimeparser: Store only one signature fingerprint. + +### Tests + +- Test expiration of ephemeral messages with unknown viewtype. +- Test expiration of non-ephemeral message with unknown viewtype. + +## [2.20.0] - 2025-10-13 + +This release fixes a bug that resulted in ephemeral loop getting stuck in infinite loop +when trying to delete a message with unknown viewtype. + +### Fixes + +- Accept unknown viewtype in ephemeral loop. +- Accept unknown viewtype in delete-old-messages loop. + ## [2.19.0] - 2025-10-12 ### Features / Changes @@ -6910,3 +6965,6 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed [2.17.0]: https://github.com/chatmail/core/compare/v2.16.0..v2.17.0 [2.18.0]: https://github.com/chatmail/core/compare/v2.17.0..v2.18.0 [2.19.0]: https://github.com/chatmail/core/compare/v2.18.0..v2.19.0 +[2.20.0]: https://github.com/chatmail/core/compare/v2.19.0..v2.20.0 +[2.21.0]: https://github.com/chatmail/core/compare/v2.20.0..v2.21.0 +[2.22.0]: https://github.com/chatmail/core/compare/v2.21.0..v2.22.0 diff --git a/Cargo.lock b/Cargo.lock index 83e88ac597..d574d22f74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -346,15 +346,15 @@ dependencies = [ [[package]] name = "async_zip" -version = "0.0.17" +version = "0.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52" +checksum = "0d8c50d65ce1b0e0cb65a785ff615f78860d7754290647d3b983208daa4f85e6" dependencies = [ "async-compression", "crc32fast", "futures-lite", "pin-project", - "thiserror 1.0.69", + "thiserror 2.0.17", "tokio", "tokio-util", ] @@ -1289,7 +1289,7 @@ dependencies = [ [[package]] name = "deltachat" -version = "2.19.0" +version = "2.22.0" dependencies = [ "anyhow", "async-broadcast", @@ -1316,7 +1316,6 @@ dependencies = [ "futures", "futures-lite", "hex", - "hickory-resolver", "http-body-util", "humansize", "hyper", @@ -1346,7 +1345,6 @@ dependencies = [ "ratelimit", "regex", "rusqlite", - "rustls", "rustls-pki-types", "sanitize-filename", "sdp", @@ -1399,7 +1397,7 @@ dependencies = [ [[package]] name = "deltachat-jsonrpc" -version = "2.19.0" +version = "2.22.0" dependencies = [ "anyhow", "async-channel 2.5.0", @@ -1421,7 +1419,7 @@ dependencies = [ [[package]] name = "deltachat-repl" -version = "2.19.0" +version = "2.22.0" dependencies = [ "anyhow", "deltachat", @@ -1437,7 +1435,7 @@ dependencies = [ [[package]] name = "deltachat-rpc-server" -version = "2.19.0" +version = "2.22.0" dependencies = [ "anyhow", "deltachat", @@ -1466,7 +1464,7 @@ dependencies = [ [[package]] name = "deltachat_ffi" -version = "2.19.0" +version = "2.22.0" dependencies = [ "anyhow", "deltachat", diff --git a/Cargo.toml b/Cargo.toml index eef73d4e9f..43bd54326c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat" -version = "2.19.0" +version = "2.22.0" edition = "2024" license = "MPL-2.0" rust-version = "1.85" @@ -47,7 +47,7 @@ async-channel = { workspace = true } async-imap = { version = "0.11.1", default-features = false, features = ["runtime-tokio", "compress"] } async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] } async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] } -async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] } +async_zip = { version = "0.0.18", default-features = false, features = ["deflate", "tokio-fs"] } base64 = { workspace = true } blake3 = "1.8.2" brotli = { version = "8", default-features=false, features = ["std"] } @@ -61,7 +61,6 @@ fd-lock = "4" futures-lite = { workspace = true } futures = { workspace = true } hex = "0.4.0" -hickory-resolver = "0.25.2" http-body-util = "0.1.3" humansize = "2" hyper = "1" @@ -87,7 +86,6 @@ rand = { workspace = true } regex = { workspace = true } rusqlite = { workspace = true, features = ["sqlcipher"] } rustls-pki-types = "1.12.0" -rustls = { version = "0.23.22", default-features = false } sanitize-filename = { workspace = true } sdp = "0.8.0" serde_json = { workspace = true } diff --git a/deltachat-ffi/Cargo.toml b/deltachat-ffi/Cargo.toml index 5e60f13fd1..67f2b4f87c 100644 --- a/deltachat-ffi/Cargo.toml +++ b/deltachat-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat_ffi" -version = "2.19.0" +version = "2.22.0" description = "Deltachat FFI" edition = "2018" readme = "README.md" diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 053f5fc120..2fd419fc90 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -1763,9 +1763,7 @@ dc_chat_t* dc_get_chat (dc_context_t* context, uint32_t ch * * @memberof dc_context_t * @param context The context object. - * @param protect If set to 1 the function creates group with protection initially enabled. - * Only verified members are allowed in these groups - * and end-to-end-encryption is always enabled. + * @param protect Deprecated 2025-08-31, ignored. * @param name The name of the group chat to create. * The name may be changed later using dc_set_chat_name(). * To find out the name of a group later, see dc_chat_get_name() @@ -3890,18 +3888,12 @@ int dc_chat_can_send (const dc_chat_t* chat); /** - * Check if a chat is protected. - * - * Only verified contacts - * as determined by dc_contact_is_verified() - * can be added to protected chats. - * - * Protected chats are created using dc_create_group_chat() - * by setting the 'protect' parameter to 1. + * Deprecated, always returns 0. * * @memberof dc_chat_t * @param chat The chat object. - * @return 1=chat protected, 0=chat is not protected. + * @return Always 0. + * @deprecated 2025-09-09 */ int dc_chat_is_protected (const dc_chat_t* chat); @@ -5350,11 +5342,9 @@ dc_provider_t* dc_provider_new_from_email (const dc_context_t* conte /** - * Create a provider struct for the given e-mail address by local and DNS lookup. + * Create a provider struct for the given e-mail address by local lookup. * - * First lookup is done from the local database as of dc_provider_new_from_email(). - * If the first lookup fails, an additional DNS lookup is done, - * trying to figure out the provider belonging to custom domains. + * DNS lookup is not used anymore and this function is deprecated. * * @memberof dc_provider_t * @param context The context object. @@ -5362,6 +5352,7 @@ dc_provider_t* dc_provider_new_from_email (const dc_context_t* conte * @return A dc_provider_t struct which can be used with the dc_provider_get_* * accessor functions. If no provider info is found, NULL will be * returned. + * @deprecated 2025-10-17 use dc_provider_new_from_email() instead. */ dc_provider_t* dc_provider_new_from_email_with_dns (const dc_context_t* context, const char* email); @@ -6965,11 +6956,6 @@ void dc_event_unref(dc_event_t* event); /// Used to build the string returned by dc_get_contact_encrinfo(). #define DC_STR_ENCR_NONE 28 -/// "This message was encrypted for another setup." -/// -/// Used as message text if decryption fails. -#define DC_STR_CANTDECRYPT_MSG_BODY 29 - /// "Fingerprints" /// /// Used to build the string returned by dc_get_contact_encrinfo(). @@ -7687,12 +7673,6 @@ void dc_event_unref(dc_event_t* event); /// `%1$s` will be replaced by the provider's domain. #define DC_STR_INVALID_UNENCRYPTED_MAIL 174 -/// "⚠️ It seems you are using Delta Chat on multiple devices that cannot decrypt each other's outgoing messages. To fix this, on the older device use \"Settings / Add Second Device\" and follow the instructions." -/// -/// Added to the device chat if could not decrypt a new outgoing message (i.e. not when fetching -/// existing messages). But no more than once a day. -#define DC_STR_CANT_DECRYPT_OUTGOING_MSGS 175 - /// "You reacted %1$s to '%2$s'" /// /// `%1$s` will be replaced by the reaction, usually an emoji diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 96157e4bbe..9c768d7366 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -22,7 +22,7 @@ use std::sync::{Arc, LazyLock}; use std::time::{Duration, SystemTime}; use anyhow::Context as _; -use deltachat::chat::{ChatId, ChatVisibility, MessageListOptions, MuteDuration, ProtectionStatus}; +use deltachat::chat::{ChatId, ChatVisibility, MessageListOptions, MuteDuration}; use deltachat::constants::DC_MSG_ID_LAST_SPECIAL; use deltachat::contact::{Contact, ContactId, Origin}; use deltachat::context::{Context, ContextBuilder}; @@ -1721,7 +1721,7 @@ pub unsafe extern "C" fn dc_get_chat(context: *mut dc_context_t, chat_id: u32) - #[no_mangle] pub unsafe extern "C" fn dc_create_group_chat( context: *mut dc_context_t, - protect: libc::c_int, + _protect: libc::c_int, name: *const libc::c_char, ) -> u32 { if context.is_null() || name.is_null() { @@ -1729,22 +1729,12 @@ pub unsafe extern "C" fn dc_create_group_chat( return 0; } let ctx = &*context; - let Some(protect) = ProtectionStatus::from_i32(protect) - .context("Bad protect-value for dc_create_group_chat()") - .log_err(ctx) - .ok() - else { - return 0; - }; - block_on(async move { - chat::create_group_chat(ctx, protect, &to_string_lossy(name)) - .await - .context("Failed to create group chat") - .log_err(ctx) - .map(|id| id.to_u32()) - .unwrap_or(0) - }) + block_on(chat::create_group(ctx, &to_string_lossy(name))) + .context("Failed to create group chat") + .log_err(ctx) + .map(|id| id.to_u32()) + .unwrap_or(0) } #[no_mangle] @@ -3206,13 +3196,8 @@ pub unsafe extern "C" fn dc_chat_can_send(chat: *mut dc_chat_t) -> libc::c_int { } #[no_mangle] -pub unsafe extern "C" fn dc_chat_is_protected(chat: *mut dc_chat_t) -> libc::c_int { - if chat.is_null() { - eprintln!("ignoring careless call to dc_chat_is_protected()"); - return 0; - } - let ffi_chat = &*chat; - ffi_chat.chat.is_protected() as libc::c_int +pub extern "C" fn dc_chat_is_protected(_chat: *mut dc_chat_t) -> libc::c_int { + 0 } #[no_mangle] @@ -4661,13 +4646,9 @@ pub unsafe extern "C" fn dc_provider_new_from_email( let ctx = &*context; - match block_on(provider::get_provider_info_by_addr( - ctx, - addr.as_str(), - true, - )) - .log_err(ctx) - .unwrap_or_default() + match provider::get_provider_info_by_addr(addr.as_str()) + .log_err(ctx) + .unwrap_or_default() { Some(provider) => provider, None => ptr::null_mut(), @@ -4686,25 +4667,13 @@ pub unsafe extern "C" fn dc_provider_new_from_email_with_dns( let addr = to_string_lossy(addr); let ctx = &*context; - let proxy_enabled = block_on(ctx.get_config_bool(config::Config::ProxyEnabled)) - .context("Can't get config") - .log_err(ctx); - match proxy_enabled { - Ok(proxy_enabled) => { - match block_on(provider::get_provider_info_by_addr( - ctx, - addr.as_str(), - proxy_enabled, - )) - .log_err(ctx) - .unwrap_or_default() - { - Some(provider) => provider, - None => ptr::null_mut(), - } - } - Err(_) => ptr::null_mut(), + match provider::get_provider_info_by_addr(addr.as_str()) + .log_err(ctx) + .unwrap_or_default() + { + Some(provider) => provider, + None => ptr::null_mut(), } } diff --git a/deltachat-jsonrpc/Cargo.toml b/deltachat-jsonrpc/Cargo.toml index 5515beef41..7e3956e59d 100644 --- a/deltachat-jsonrpc/Cargo.toml +++ b/deltachat-jsonrpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-jsonrpc" -version = "2.19.0" +version = "2.22.0" description = "DeltaChat JSON-RPC API" edition = "2021" license = "MPL-2.0" diff --git a/deltachat-jsonrpc/src/api.rs b/deltachat-jsonrpc/src/api.rs index bc2a8f285f..8b9fb648cc 100644 --- a/deltachat-jsonrpc/src/api.rs +++ b/deltachat-jsonrpc/src/api.rs @@ -12,7 +12,6 @@ use deltachat::calls::ice_servers; use deltachat::chat::{ self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex, marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions, - ProtectionStatus, }; use deltachat::chatlist::Chatlist; use deltachat::config::Config; @@ -336,21 +335,10 @@ impl CommandApi { /// instead of the domain. async fn get_provider_info( &self, - account_id: u32, + _account_id: u32, email: String, ) -> Result> { - let ctx = self.get_context(account_id).await?; - - let proxy_enabled = ctx - .get_config_bool(deltachat::config::Config::ProxyEnabled) - .await?; - - let provider_info = get_provider_info( - &ctx, - email.split('@').next_back().unwrap_or(""), - proxy_enabled, - ) - .await; + let provider_info = get_provider_info(email.split('@').next_back().unwrap_or("")); Ok(ProviderInfo::from_dc_type(provider_info)) } @@ -978,18 +966,9 @@ impl CommandApi { /// /// To check, if a chat is still unpromoted, you can look at the `is_unpromoted` property of `BasicChat` or `FullChat`. /// This may be useful if you want to show some help for just created groups. - /// - /// @param protect If set to 1 the function creates group with protection initially enabled. - /// Only verified members are allowed in these groups - async fn create_group_chat(&self, account_id: u32, name: String, protect: bool) -> Result { + async fn create_group_chat(&self, account_id: u32, name: String) -> Result { let ctx = self.get_context(account_id).await?; - let protect = match protect { - true => ProtectionStatus::Protected, - false => ProtectionStatus::Unprotected, - }; - chat::create_group_ex(&ctx, Some(protect), &name) - .await - .map(|id| id.to_u32()) + chat::create_group(&ctx, &name).await.map(|id| id.to_u32()) } /// Create a new unencrypted group chat. @@ -998,7 +977,7 @@ impl CommandApi { /// address-contacts. async fn create_group_chat_unencrypted(&self, account_id: u32, name: String) -> Result { let ctx = self.get_context(account_id).await?; - chat::create_group_ex(&ctx, None, &name) + chat::create_group_unencrypted(&ctx, &name) .await .map(|id| id.to_u32()) } diff --git a/deltachat-jsonrpc/src/api/types/chat.rs b/deltachat-jsonrpc/src/api/types/chat.rs index 86f78abee0..6b5e693043 100644 --- a/deltachat-jsonrpc/src/api/types/chat.rs +++ b/deltachat-jsonrpc/src/api/types/chat.rs @@ -19,18 +19,6 @@ pub struct FullChat { id: u32, name: String, - /// True if the chat is protected. - /// - /// Only verified contacts - /// as determined by [`ContactObject::is_verified`] / `Contact.isVerified` - /// can be added to protected chats. - /// - /// Protected chats are created using [`create_group_chat`] / `createGroupChat()` - /// by setting the 'protect' parameter to true. - /// - /// [`create_group_chat`]: crate::api::CommandApi::create_group_chat - 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 "key-contacts", @@ -131,7 +119,6 @@ impl FullChat { Ok(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, @@ -172,18 +159,6 @@ pub struct BasicChat { id: u32, name: String, - /// True if the chat is protected. - /// - /// UI should display a green checkmark - /// in the chat title, - /// in the chat profile title and - /// in the chatlist item - /// if chat protection is enabled. - /// UI should also display a green checkmark - /// 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 "key-contacts", @@ -234,7 +209,6 @@ impl BasicChat { Ok(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, diff --git a/deltachat-jsonrpc/src/api/types/chat_list.rs b/deltachat-jsonrpc/src/api/types/chat_list.rs index b5d31a7913..47e72524c0 100644 --- a/deltachat-jsonrpc/src/api/types/chat_list.rs +++ b/deltachat-jsonrpc/src/api/types/chat_list.rs @@ -30,7 +30,6 @@ pub enum ChatListItemFetchResult { summary_status: u32, /// showing preview if last chat message is image summary_preview_image: Option, - is_protected: bool, /// True if the chat is encrypted. /// This means that all messages in the chat are encrypted, @@ -161,7 +160,6 @@ pub(crate) async fn get_chat_list_item_by_id( summary_text2, 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, diff --git a/deltachat-jsonrpc/src/api/types/contact.rs b/deltachat-jsonrpc/src/api/types/contact.rs index 9a98a75f98..bafdfb5cc6 100644 --- a/deltachat-jsonrpc/src/api/types/contact.rs +++ b/deltachat-jsonrpc/src/api/types/contact.rs @@ -1,6 +1,6 @@ use anyhow::Result; -use deltachat::color; use deltachat::context::Context; +use deltachat::key::{DcKey, SignedPublicKey}; use serde::Serialize; use typescript_type_def::TypeDef; @@ -130,7 +130,13 @@ pub struct VcardContact { impl From for VcardContact { fn from(vc: deltachat_contact_tools::VcardContact) -> Self { let display_name = vc.display_name().to_string(); - let color = color::str_to_color(&vc.addr.to_lowercase()); + let is_self = false; + let fpr = vc.key.as_deref().and_then(|k| { + SignedPublicKey::from_base64(k) + .ok() + .map(|k| k.dc_fingerprint()) + }); + let color = deltachat::contact::get_color(is_self, &vc.addr, &fpr); Self { addr: vc.addr, display_name, diff --git a/deltachat-jsonrpc/src/api/types/message.rs b/deltachat-jsonrpc/src/api/types/message.rs index fd47c534db..b2832db94b 100644 --- a/deltachat-jsonrpc/src/api/types/message.rs +++ b/deltachat-jsonrpc/src/api/types/message.rs @@ -532,7 +532,6 @@ pub struct MessageSearchResult { chat_color: String, chat_name: String, chat_type: u32, - is_chat_protected: bool, is_chat_contact_request: bool, is_chat_archived: bool, message: String, @@ -572,7 +571,6 @@ impl MessageSearchResult { chat_color, chat_type: chat.get_type().to_u32().context("unknown chat type id")?, chat_profile_image, - is_chat_protected: chat.is_protected(), is_chat_contact_request: chat.is_contact_request(), is_chat_archived: chat.get_visibility() == ChatVisibility::Archived, message: message.get_text(), diff --git a/deltachat-jsonrpc/typescript/package.json b/deltachat-jsonrpc/typescript/package.json index e27f81c0bc..70648e4e35 100644 --- a/deltachat-jsonrpc/typescript/package.json +++ b/deltachat-jsonrpc/typescript/package.json @@ -54,5 +54,5 @@ }, "type": "module", "types": "dist/deltachat.d.ts", - "version": "2.19.0" + "version": "2.22.0" } diff --git a/deltachat-repl/Cargo.toml b/deltachat-repl/Cargo.toml index 07815796e0..60d0a9669f 100644 --- a/deltachat-repl/Cargo.toml +++ b/deltachat-repl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-repl" -version = "2.19.0" +version = "2.22.0" license = "MPL-2.0" edition = "2021" repository = "https://github.com/chatmail/core" diff --git a/deltachat-repl/src/cmdline.rs b/deltachat-repl/src/cmdline.rs index 079486fb22..d48ad4b58e 100644 --- a/deltachat-repl/src/cmdline.rs +++ b/deltachat-repl/src/cmdline.rs @@ -6,9 +6,7 @@ use std::str::FromStr; use std::time::Duration; use anyhow::{bail, ensure, Result}; -use deltachat::chat::{ - self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration, ProtectionStatus, -}; +use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration}; use deltachat::chatlist::*; use deltachat::constants::*; use deltachat::contact::*; @@ -347,7 +345,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu createchat \n\ creategroup \n\ createbroadcast \n\ - createprotected \n\ addmember \n\ removemember \n\ groupname \n\ @@ -358,6 +355,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu dellocations\n\ getlocations []\n\ send \n\ + send-sync \n\ sendempty\n\ sendimage []\n\ sendsticker []\n\ @@ -562,7 +560,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu for i in (0..cnt).rev() { let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)?).await?; println!( - "{}#{}: {} [{} fresh] {}{}{}{}", + "{}#{}: {} [{} fresh] {}{}{}", chat_prefix(&chat), chat.get_id(), chat.get_name(), @@ -573,7 +571,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu ChatVisibility::Archived => "πŸ“¦", ChatVisibility::Pinned => "πŸ“Œ", }, - if chat.is_protected() { "πŸ›‘οΈ" } else { "" }, if chat.is_contact_request() { "πŸ†•" } else { @@ -688,7 +685,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu format!("{} member(s)", members.len()) }; println!( - "{}#{}: {} [{}]{}{}{} {}", + "{}#{}: {} [{}]{}{}{}", chat_prefix(sel_chat), sel_chat.get_id(), sel_chat.get_name(), @@ -706,11 +703,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu }, _ => "".to_string(), }, - if sel_chat.is_protected() { - "πŸ›‘οΈ" - } else { - "" - }, ); log_msglist(&context, &msglist).await?; if let Some(draft) = sel_chat.get_id().get_draft(&context).await? { @@ -739,8 +731,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu } "creategroup" => { ensure!(!arg1.is_empty(), "Argument missing."); - let chat_id = - chat::create_group_chat(&context, ProtectionStatus::Unprotected, arg1).await?; + let chat_id = chat::create_group(&context, arg1).await?; println!("Group#{chat_id} created successfully."); } @@ -750,13 +741,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu println!("Broadcast#{chat_id} created successfully."); } - "createprotected" => { - ensure!(!arg1.is_empty(), "Argument missing."); - let chat_id = - chat::create_group_chat(&context, ProtectionStatus::Protected, arg1).await?; - - println!("Group#{chat_id} created and protected successfully."); - } "addmember" => { ensure!(sel_chat.is_some(), "No chat selected"); ensure!(!arg1.is_empty(), "Argument missing."); @@ -908,6 +892,23 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), msg).await?; } + "send-sync" => { + ensure!(sel_chat.is_some(), "No chat selected."); + ensure!(!arg1.is_empty(), "No message text given."); + + // Send message over a dedicated SMTP connection + // and measure time. + // + // This can be used to benchmark SMTP connection establishment. + let time_start = std::time::Instant::now(); + + let msg = format!("{arg1} {arg2}"); + let mut msg = Message::new_text(msg); + chat::send_msg_sync(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?; + + let time_needed = time_start.elapsed(); + println!("Sent message in {time_needed:?}."); + } "sendempty" => { ensure!(sel_chat.is_some(), "No chat selected."); chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), "".into()).await?; @@ -1248,10 +1249,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu } "providerinfo" => { ensure!(!arg1.is_empty(), "Argument missing."); - let proxy_enabled = context - .get_config_bool(config::Config::ProxyEnabled) - .await?; - match provider::get_provider_info(&context, arg1, proxy_enabled).await { + match provider::get_provider_info(arg1) { Some(info) => { println!("Information for provider belonging to {arg1}:"); println!("status: {}", info.status as u32); diff --git a/deltachat-repl/src/main.rs b/deltachat-repl/src/main.rs index bea6bac99e..3b63667fe8 100644 --- a/deltachat-repl/src/main.rs +++ b/deltachat-repl/src/main.rs @@ -179,7 +179,7 @@ const DB_COMMANDS: [&str; 11] = [ "housekeeping", ]; -const CHAT_COMMANDS: [&str; 38] = [ +const CHAT_COMMANDS: [&str; 39] = [ "listchats", "listarchived", "start-realtime", @@ -199,6 +199,7 @@ const CHAT_COMMANDS: [&str; 38] = [ "dellocations", "getlocations", "send", + "send-sync", "sendempty", "sendimage", "sendsticker", diff --git a/deltachat-rpc-client/pyproject.toml b/deltachat-rpc-client/pyproject.toml index 1cea357adb..a48210b1d7 100644 --- a/deltachat-rpc-client/pyproject.toml +++ b/deltachat-rpc-client/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "deltachat-rpc-client" -version = "2.19.0" +version = "2.22.0" description = "Python client for Delta Chat core JSON-RPC interface" classifiers = [ "Development Status :: 5 - Production/Stable", diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/account.py b/deltachat-rpc-client/src/deltachat_rpc_client/account.py index e1f0667201..53d77f7ce4 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/account.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/account.py @@ -300,7 +300,7 @@ def get_chatlist( chats.append(AttrDict(item)) return chats - def create_group(self, name: str, protect: bool = False) -> Chat: + def create_group(self, name: str) -> Chat: """Create a new group chat. After creation, @@ -317,12 +317,8 @@ def create_group(self, name: str, protect: bool = False) -> Chat: To check, if a chat is still unpromoted, you can look at the `is_unpromoted` property of a chat (see `get_full_snapshot()` / `get_basic_snapshot()`). This may be useful if you want to show some help for just created groups. - - :param protect: If set to 1 the function creates group with protection initially enabled. - Only verified members are allowed in these groups - and end-to-end-encryption is always enabled. """ - return Chat(self, self._rpc.create_group_chat(self.id, name, protect)) + return Chat(self, self._rpc.create_group_chat(self.id, name)) def create_broadcast(self, name: str) -> Chat: """Create a new **broadcast channel** diff --git a/deltachat-rpc-client/tests/test_calls.py b/deltachat-rpc-client/tests/test_calls.py index 6ea4810fcf..e83d736f04 100644 --- a/deltachat-rpc-client/tests/test_calls.py +++ b/deltachat-rpc-client/tests/test_calls.py @@ -9,6 +9,7 @@ def test_calls(acfactory) -> None: alice_contact_bob = alice.create_contact(bob, "Bob") alice_chat_bob = alice_contact_bob.create_chat() + bob.create_chat(alice) # Accept the chat so incoming call causes a notification. outgoing_call_message = alice_chat_bob.place_outgoing_call(place_call_info) assert outgoing_call_message.get_call_info().state.kind == "Alerting" @@ -67,6 +68,7 @@ def test_video_call(acfactory) -> None: alice, bob = acfactory.get_online_accounts(2) + bob.create_chat(alice) # Accept the chat so incoming call causes a notification. alice_contact_bob = alice.create_contact(bob, "Bob") alice_chat_bob = alice_contact_bob.create_chat() alice_chat_bob.place_outgoing_call(place_call_info) @@ -84,3 +86,24 @@ def test_ice_servers(acfactory) -> None: ice_servers = alice.ice_servers() assert len(ice_servers) == 1 + + +def test_no_contact_request_call(acfactory) -> None: + alice, bob = acfactory.get_online_accounts(2) + + alice_chat_bob = alice.create_chat(bob) + alice_chat_bob.place_outgoing_call("offer") + alice_chat_bob.send_text("Hello!") + + # Notification for "Hello!" message should arrive + # without the call ringing. + while True: + event = bob.wait_for_event() + + # There should be no incoming call notification. + assert event.kind != EventType.INCOMING_CALL + + if event.kind == EventType.INCOMING_MSG: + msg = bob.get_message_by_id(event.msg_id) + assert msg.get_snapshot().text == "Hello!" + break diff --git a/deltachat-rpc-client/tests/test_securejoin.py b/deltachat-rpc-client/tests/test_securejoin.py index 31f4835207..d77108c59d 100644 --- a/deltachat-rpc-client/tests/test_securejoin.py +++ b/deltachat-rpc-client/tests/test_securejoin.py @@ -58,8 +58,7 @@ def test_qr_setup_contact_svg(acfactory) -> None: assert "Alice" in svg -@pytest.mark.parametrize("protect", [True, False]) -def test_qr_securejoin(acfactory, protect): +def test_qr_securejoin(acfactory): alice, bob, fiona = acfactory.get_online_accounts(3) # Setup second device for Alice @@ -67,8 +66,7 @@ def test_qr_securejoin(acfactory, protect): alice2 = alice.clone() logging.info("Alice creates a group") - alice_chat = alice.create_group("Group", protect=protect) - assert alice_chat.get_basic_snapshot().is_protected == protect + alice_chat = alice.create_group("Group") logging.info("Bob joins the group") qr_code = alice_chat.get_qr_code() @@ -89,7 +87,6 @@ def test_qr_securejoin(acfactory, protect): snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot() assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr")) - assert snapshot.chat.get_basic_snapshot().is_protected == protect # Test that Bob verified Alice's profile. bob_contact_alice = bob.create_contact(alice) @@ -125,8 +122,8 @@ def test_qr_securejoin_contact_request(acfactory) -> None: bob_chat_alice = snapshot.chat assert bob_chat_alice.get_basic_snapshot().is_contact_request - alice_chat = alice.create_group("Verified group", protect=True) - logging.info("Bob joins verified group") + alice_chat = alice.create_group("Group") + logging.info("Bob joins the group") qr_code = alice_chat.get_qr_code() bob.secure_join(qr_code) while True: @@ -150,8 +147,8 @@ def test_qr_readreceipt(acfactory) -> None: for joiner in [bob, charlie]: joiner.wait_for_securejoin_joiner_success() - logging.info("Alice creates a verified group") - group = alice.create_group("Group", protect=True) + logging.info("Alice creates a group") + group = alice.create_group("Group") alice_contact_bob = alice.create_contact(bob, "Bob") alice_contact_charlie = alice.create_contact(charlie, "Charlie") @@ -216,11 +213,10 @@ def test_verified_group_member_added_recovery(acfactory) -> None: """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") - chat = ac1.create_group("Verified group", protect=True) - assert chat.get_basic_snapshot().is_protected + logging.info("ac1 creates a group") + chat = ac1.create_group("Group") - logging.info("ac2 joins verified group") + logging.info("ac2 joins the group") qr_code = chat.get_qr_code() ac2.secure_join(qr_code) ac2.wait_for_securejoin_joiner_success() @@ -302,8 +298,8 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory): # we first create a fully joined verified group, and then start # joining a second time but interrupt it, to create pending bob state - logging.info("ac1: create verified group that ac2 fully joins") - ch1 = ac1.create_group("Group", protect=True) + logging.info("ac1: create a group that ac2 fully joins") + ch1 = ac1.create_group("Group") qr_code = ch1.get_qr_code() ac2.secure_join(qr_code) ac1.wait_for_securejoin_inviter_success() @@ -313,7 +309,6 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory): while 1: snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot() if snapshot.text == "ac1 says hello": - assert snapshot.chat.get_basic_snapshot().is_protected break logging.info("ac1: let ac2 join again but shutoff ac1 in the middle of securejoin") @@ -327,7 +322,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory): 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 = ac3.create_group("ac3-created") vg.add_contact(ac3.create_contact(ac2)) # ensure ac2 receives message in VG @@ -335,7 +330,6 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory): while 1: msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot() if msg.text == "hello": - assert msg.chat.get_basic_snapshot().is_protected break logging.info("ac3: create a join-code for group VG and let ac4 join, check that ac2 got it") @@ -359,7 +353,7 @@ def test_qr_new_group_unblocked(acfactory): """ ac1, ac2 = acfactory.get_online_accounts(2) - ac1_chat = ac1.create_group("Group for joining", protect=True) + ac1_chat = ac1.create_group("Group for joining") qr_code = ac1_chat.get_qr_code() ac2.secure_join(qr_code) @@ -384,8 +378,7 @@ def test_aeap_flow_verified(acfactory): addr, password = acfactory.get_credentials() logging.info("ac1: create verified-group QR, ac2 scans and joins") - chat = ac1.create_group("hello", protect=True) - assert chat.get_basic_snapshot().is_protected + chat = ac1.create_group("hello") qr_code = chat.get_qr_code() logging.info("ac2: start QR-code based join-group protocol") ac2.secure_join(qr_code) @@ -439,7 +432,6 @@ def test_gossip_verification(acfactory) -> None: logging.info("Bob creates an Autocrypt group") bob_group_chat = bob.create_group("Autocrypt Group") - assert not bob_group_chat.get_basic_snapshot().is_protected bob_group_chat.add_contact(bob_contact_alice) bob_group_chat.add_contact(bob_contact_carol) bob_group_chat.send_message(text="Hello Autocrypt group") @@ -448,13 +440,12 @@ def test_gossip_verification(acfactory) -> None: assert snapshot.text == "Hello Autocrypt group" assert snapshot.show_padlock - # Autocrypt group does not propagate verification. + # Group propagates verification using Autocrypt-Gossip header. carol_contact_alice_snapshot = carol_contact_alice.get_snapshot() - assert not carol_contact_alice_snapshot.is_verified + assert carol_contact_alice_snapshot.is_verified logging.info("Bob creates a Securejoin group") - bob_group_chat = bob.create_group("Securejoin Group", protect=True) - assert bob_group_chat.get_basic_snapshot().is_protected + bob_group_chat = bob.create_group("Securejoin Group") bob_group_chat.add_contact(bob_contact_alice) bob_group_chat.add_contact(bob_contact_carol) bob_group_chat.send_message(text="Hello Securejoin group") @@ -477,7 +468,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None: ac1, ac2, ac3 = acfactory.get_online_accounts(3) # ac3 creates protected group with ac1. - ac3_chat = ac3.create_group("Verified group", protect=True) + ac3_chat = ac3.create_group("Group") # ac1 joins ac3 group. ac3_qr_code = ac3_chat.get_qr_code() @@ -525,7 +516,6 @@ def test_securejoin_after_contact_resetup(acfactory) -> None: snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot() assert snapshot.is_info ac2_chat = snapshot.chat - assert ac2_chat.get_basic_snapshot().is_protected assert len(ac2_chat.get_contacts()) == 3 # ac1 is still "not verified" for ac2 due to inconsistent state. @@ -535,9 +525,8 @@ def test_securejoin_after_contact_resetup(acfactory) -> None: def test_withdraw_securejoin_qr(acfactory): alice, bob = acfactory.get_online_accounts(2) - logging.info("Alice creates a verified group") - alice_chat = alice.create_group("Verified group", protect=True) - assert alice_chat.get_basic_snapshot().is_protected + logging.info("Alice creates a group") + alice_chat = alice.create_group("Group") logging.info("Bob joins verified group") qr_code = alice_chat.get_qr_code() @@ -548,7 +537,6 @@ def test_withdraw_securejoin_qr(acfactory): snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot() assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr")) - assert snapshot.chat.get_basic_snapshot().is_protected bob_chat.leave() snapshot = alice.get_message_by_id(alice.wait_for_msgs_changed_event().msg_id).get_snapshot() diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index f7c144a4a4..7f437f14ab 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -570,8 +570,11 @@ def test_provider_info(rpc) -> None: assert provider_info is None # Test MX record resolution. + # This previously resulted in Gmail provider + # because MX record pointed to google.com domain, + # but MX record resolution has been removed. provider_info = rpc.get_provider_info(account_id, "github.com") - assert provider_info["id"] == "gmail" + assert provider_info is None # Disable MX record resolution. rpc.set_config(account_id, "proxy_enabled", "1") diff --git a/deltachat-rpc-client/tests/test_vcard.py b/deltachat-rpc-client/tests/test_vcard.py index 53e8348b33..c0fdb2c94c 100644 --- a/deltachat-rpc-client/tests/test_vcard.py +++ b/deltachat-rpc-client/tests/test_vcard.py @@ -1,8 +1,11 @@ def test_vcard(acfactory) -> None: - alice, bob = acfactory.get_online_accounts(2) + alice, bob, fiona = acfactory.get_online_accounts(3) alice_contact_bob = alice.create_contact(bob, "Bob") alice_contact_charlie = alice.create_contact("charlie@example.org", "Charlie") + alice_contact_charlie_snapshot = alice_contact_charlie.get_snapshot() + alice_contact_fiona = alice.create_contact(fiona, "Fiona") + alice_contact_fiona_snapshot = alice_contact_fiona.get_snapshot() alice_chat_bob = alice_contact_bob.create_chat() alice_chat_bob.send_contact(alice_contact_charlie) @@ -12,3 +15,12 @@ def test_vcard(acfactory) -> None: snapshot = message.get_snapshot() assert snapshot.vcard_contact assert snapshot.vcard_contact.addr == "charlie@example.org" + assert snapshot.vcard_contact.color == alice_contact_charlie_snapshot.color + + alice_chat_bob.send_contact(alice_contact_fiona) + event = bob.wait_for_incoming_msg_event() + message = bob.get_message_by_id(event.msg_id) + snapshot = message.get_snapshot() + assert snapshot.vcard_contact + assert snapshot.vcard_contact.key + assert snapshot.vcard_contact.color == alice_contact_fiona_snapshot.color diff --git a/deltachat-rpc-server/Cargo.toml b/deltachat-rpc-server/Cargo.toml index 1e1370afd5..9e74f072c7 100644 --- a/deltachat-rpc-server/Cargo.toml +++ b/deltachat-rpc-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-rpc-server" -version = "2.19.0" +version = "2.22.0" description = "DeltaChat JSON-RPC server" edition = "2021" readme = "README.md" diff --git a/deltachat-rpc-server/npm-package/package.json b/deltachat-rpc-server/npm-package/package.json index 628195637f..7e321f1f86 100644 --- a/deltachat-rpc-server/npm-package/package.json +++ b/deltachat-rpc-server/npm-package/package.json @@ -15,5 +15,5 @@ }, "type": "module", "types": "index.d.ts", - "version": "2.19.0" + "version": "2.22.0" } diff --git a/flake.nix b/flake.nix index 216e046e36..0576ee7856 100644 --- a/flake.nix +++ b/flake.nix @@ -98,9 +98,6 @@ nativeBuildInputs = [ pkgs.perl # Needed to build vendored OpenSSL. ]; - buildInputs = pkgs.lib.optionals isDarwin [ - pkgs.darwin.apple_sdk.frameworks.SystemConfiguration - ]; auditable = false; # Avoid cargo-auditable failures. doCheck = false; # Disable test as it requires network access. }; @@ -240,6 +237,9 @@ auditable = false; # Avoid cargo-auditable failures. doCheck = false; # Disable test as it requires network access. + CARGO_TARGET_X86_64_APPLE_DARWIN_RUSTFLAGS = "-Clink-args=-L${pkgsCross.libiconv}/lib"; + CARGO_TARGET_AARCH64_APPLE_DARWIN_RUSTFLAGS = "-Clink-args=-L${pkgsCross.libiconv}/lib"; + CARGO_BUILD_TARGET = rustTarget; TARGET_CC = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc"; CARGO_BUILD_RUSTFLAGS = [ @@ -483,12 +483,6 @@ pkgs.rustPlatform.cargoSetupHook pkgs.cargo ]; - buildInputs = pkgs.lib.optionals isDarwin [ - pkgs.darwin.apple_sdk.frameworks.CoreFoundation - pkgs.darwin.apple_sdk.frameworks.Security - pkgs.darwin.apple_sdk.frameworks.SystemConfiguration - pkgs.libiconv - ]; postInstall = '' substituteInPlace $out/include/deltachat.h \ diff --git a/python/pyproject.toml b/python/pyproject.toml index 135479b923..a2b3ea8a6d 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "deltachat" -version = "2.19.0" +version = "2.22.0" description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat" readme = "README.rst" requires-python = ">=3.8" diff --git a/python/src/deltachat/account.py b/python/src/deltachat/account.py index 647226fe2a..a75bcf4a07 100644 --- a/python/src/deltachat/account.py +++ b/python/src/deltachat/account.py @@ -404,18 +404,16 @@ def create_group_chat( self, name: str, contacts: Optional[List[Contact]] = None, - verified: bool = False, ) -> Chat: """create a new group chat object. Chats are unpromoted until the first message is sent. :param contacts: list of contacts to add - :param verified: if true only verified contacts can be added. :returns: a :class:`deltachat.chat.Chat` object. """ bytes_name = name.encode("utf8") - chat_id = lib.dc_create_group_chat(self._dc_context, int(verified), bytes_name) + chat_id = lib.dc_create_group_chat(self._dc_context, 0, bytes_name) chat = Chat(self, chat_id) if contacts is not None: for contact in contacts: diff --git a/python/src/deltachat/chat.py b/python/src/deltachat/chat.py index edc2d5ba7d..a11bc8074d 100644 --- a/python/src/deltachat/chat.py +++ b/python/src/deltachat/chat.py @@ -142,13 +142,6 @@ def can_send(self) -> bool: """ return bool(lib.dc_chat_can_send(self._dc_chat)) - def is_protected(self) -> bool: - """return True if this chat is a protected chat. - - :returns: True if chat is protected, False otherwise. - """ - return bool(lib.dc_chat_is_protected(self._dc_chat)) - def get_name(self) -> Optional[str]: """return name of this chat. diff --git a/python/src/deltachat/testplugin.py b/python/src/deltachat/testplugin.py index 15bdb6035b..77a2195f0f 100644 --- a/python/src/deltachat/testplugin.py +++ b/python/src/deltachat/testplugin.py @@ -604,20 +604,6 @@ def get_accepted_chat(self, ac1: Account, ac2: Account): ac2.create_chat(ac1) return ac1.create_chat(ac2) - def get_protected_chat(self, ac1: Account, ac2: Account): - chat = ac1.create_group_chat("Protected Group", verified=True) - qr = chat.get_join_qr() - ac2.qr_join_chat(qr) - ac2._evtracker.wait_securejoin_joiner_progress(1000) - ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED") - msg = ac2.get_message_by_id(ev.data2) - assert msg is not None - assert msg.text == "Messages are end-to-end encrypted." - msg = ac2._evtracker.wait_next_incoming_message() - assert msg is not None - assert "Member Me " in msg.text and " added by " in msg.text - return chat - def introduce_each_other(self, accounts, sending=True): to_wait = [] for i, acc in enumerate(accounts): diff --git a/python/tests/test_0_complex_or_slow.py b/python/tests/test_0_complex_or_slow.py index d92dbcacca..3915e3653d 100644 --- a/python/tests/test_0_complex_or_slow.py +++ b/python/tests/test_0_complex_or_slow.py @@ -118,8 +118,7 @@ def test_qr_verified_group_and_chatting(acfactory, lp): ac1, ac2, ac3 = acfactory.get_online_accounts(3) ac1_addr = ac1.get_self_contact().addr lp.sec("ac1: create verified-group QR, ac2 scans and joins") - chat1 = ac1.create_group_chat("hello", verified=True) - assert chat1.is_protected() + chat1 = ac1.create_group_chat("hello") qr = chat1.get_join_qr() lp.sec("ac2: start QR-code based join-group protocol") chat2 = ac2.qr_join_chat(qr) @@ -142,7 +141,6 @@ def test_qr_verified_group_and_chatting(acfactory, lp): lp.sec("ac2: read message and check that it's a verified chat") msg = ac2._evtracker.wait_next_incoming_message() assert msg.text == "hello" - assert msg.chat.is_protected() assert msg.is_encrypted() lp.sec("ac2: Check that ac2 verified ac1") @@ -173,8 +171,10 @@ def test_qr_verified_group_and_chatting(acfactory, lp): lp.sec("ac2: Check that ac1 verified ac3 for ac2") ac2_ac1_contact = ac2.get_contacts()[0] assert ac2.get_self_contact().get_verifier(ac2_ac1_contact).id == dc.const.DC_CONTACT_ID_SELF - ac2_ac3_contact = ac2.get_contacts()[1] - assert ac2.get_self_contact().get_verifier(ac2_ac3_contact).addr == ac1_addr + for ac2_contact in chat2.get_contacts(): + if ac2_contact == ac2_ac1_contact or ac2_contact.id == dc.const.DC_CONTACT_ID_SELF: + continue + assert ac2.get_self_contact().get_verifier(ac2_contact).addr == ac1_addr lp.sec("ac2: send message and let ac3 read it") chat2.send_text("hi") @@ -266,8 +266,7 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp): ac1_offl.stop_io() lp.sec("ac1: create verified-group QR, ac2 scans and joins") - chat = ac1.create_group_chat("hello", verified=True) - assert chat.is_protected() + chat = ac1.create_group_chat("hello") qr = chat.get_join_qr() lp.sec("ac2: start QR-code based join-group protocol") chat2 = ac2.qr_join_chat(qr) @@ -321,8 +320,7 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp ac1.set_avatar(avatar_path) lp.sec("ac1: create verified-group QR, ac2 scans and joins") - chat = ac1.create_group_chat("hello", verified=True) - assert chat.is_protected() + chat = ac1.create_group_chat("hello") qr = chat.get_join_qr() lp.sec("ac2: start QR-code based join-group protocol") ac2.qr_join_chat(qr) @@ -336,7 +334,6 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp assert msg_in.is_system_message() assert contact.addr == ac1.get_config("addr") chat2 = msg_in.chat - assert chat2.is_protected() assert chat2.get_messages()[0].text == "Messages are end-to-end encrypted." assert open(contact.get_profile_image(), "rb").read() == open(avatar_path, "rb").read() @@ -376,8 +373,7 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp): ac2_offl.stop_io() lp.sec("ac1: create verified-group QR, ac2 scans and joins") - chat1 = ac1.create_group_chat("hello", verified=True) - assert chat1.is_protected() + chat1 = ac1.create_group_chat("hello") qr = chat1.get_join_qr() lp.sec("ac2: start QR-code based join-group protocol") chat2 = ac2.qr_join_chat(qr) @@ -402,28 +398,17 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp): assert ac2_offl_ac1_contact.addr == ac1.get_config("addr") assert not ac2_offl_ac1_contact.is_verified() chat2_offl = msg_in.chat - assert not chat2_offl.is_protected() lp.sec("ac2: sending message re-gossiping Autocrypt keys") chat2.send_text("hi2") lp.sec("ac2_offl: receiving message") - ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED") - msg_in = ac2_offl.get_message_by_id(ev.data2) - assert msg_in.is_system_message() - assert msg_in.text == "Messages are end-to-end encrypted." - - # We need to consume one event that has data2=0 - ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED") - assert ev.data2 == 0 - ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED") msg_in = ac2_offl.get_message_by_id(ev.data2) assert not msg_in.is_system_message() assert msg_in.text == "hi2" assert msg_in.chat == chat2_offl assert msg_in.get_sender_contact().addr == ac2.get_config("addr") - assert msg_in.chat.is_protected() assert ac2_offl_ac1_contact.is_verified() diff --git a/python/tests/test_1_online.py b/python/tests/test_1_online.py index 33909b8dbf..ee306275d0 100644 --- a/python/tests/test_1_online.py +++ b/python/tests/test_1_online.py @@ -1204,7 +1204,7 @@ def assert_account_is_proper(ac): 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 + that resulted in failure to propagate verification when the database already contained the contact with a different email address capitalization. """ @@ -1215,17 +1215,17 @@ def test_qr_email_capitalization(acfactory, lp): lp.sec(f"ac1 creates a contact for ac2 ({ac2_addr_uppercase})") ac1.create_contact(ac2_addr_uppercase) - lp.sec("ac3 creates a verified group with a QR code") - chat = ac3.create_group_chat("hello", verified=True) + lp.sec("ac3 creates a group with a QR code") + chat = ac3.create_group_chat("hello") qr = chat.get_join_qr() - lp.sec("ac1 joins a verified group via a QR code") + lp.sec("ac1 joins a group via a QR code") ac1_chat = ac1.qr_join_chat(qr) msg = ac1._evtracker.wait_next_incoming_message() assert msg.text == "Member Me added by {}.".format(ac3.get_config("addr")) assert len(ac1_chat.get_contacts()) == 2 - lp.sec("ac2 joins a verified group via a QR code") + lp.sec("ac2 joins a group via a QR code") ac2.qr_join_chat(qr) ac1._evtracker.wait_next_incoming_message() diff --git a/python/tests/test_3_offline.py b/python/tests/test_3_offline.py index 104041b000..8701143210 100644 --- a/python/tests/test_3_offline.py +++ b/python/tests/test_3_offline.py @@ -271,10 +271,9 @@ def test_group_chat_creation_with_translation(self, ac1): chat.set_name("Homework") assert chat.get_messages()[-1].text == "abc homework xyz Homework" - @pytest.mark.parametrize("verified", [True, False]) - def test_group_chat_qr(self, acfactory, ac1, verified): + def test_group_chat_qr(self, acfactory, ac1): ac2 = acfactory.get_pseudo_configured_account() - chat = ac1.create_group_chat(name="title1", verified=verified) + chat = ac1.create_group_chat(name="title1") assert chat.is_group() qr = chat.get_join_qr() assert ac2.check_qr(qr).is_ask_verifygroup diff --git a/release-date.in b/release-date.in index 58f5a59998..2fd82012f1 100644 --- a/release-date.in +++ b/release-date.in @@ -1 +1 @@ -2025-10-12 \ No newline at end of file +2025-10-17 \ No newline at end of file diff --git a/spec.md b/spec.md index 2d8a8fc8f6..995cd30273 100644 --- a/spec.md +++ b/spec.md @@ -1,6 +1,6 @@ # Chatmail Specification -Version: 0.36.0 +Version: 0.37.0 Status: In-progress Format: [Semantic Line Breaks](https://sembr.org/) @@ -582,6 +582,24 @@ and e.g. simply search for the line starting with `EMAIL` in order to get the email address. +# Verifications + +Keys obtained using [SecureJoin](https://securejoin.readthedocs.io) protocol +and corresponding contacts +are considered "verified". + +As an extension to `Autocrypt-Gossip` header, +chatmail clients can add `_verified=1` attribute +(underscore marks the attribute as non-critical) +to indicate that they have the gossiped key +and the corresponding contact marked as verified. + +When receiving such `Autocrypt-Gossip` header +in a message signed by a verified key, +chatmail clients mark the gossiped key +as indirectly verified. + + # Transitioning to a new e-mail address (AEAP) When receiving a message: diff --git a/src/blob.rs b/src/blob.rs index 1c1630d2d5..ca47c65a4d 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -537,7 +537,11 @@ fn file_hash(src: &Path) -> Result { fn image_metadata(file: &std::fs::File) -> Result<(u64, Option)> { let len = file.metadata()?.len(); let mut bufreader = std::io::BufReader::new(file); - let exif = exif::Reader::new().read_from_container(&mut bufreader).ok(); + let exif = exif::Reader::new() + .continue_on_error(true) + .read_from_container(&mut bufreader) + .or_else(|e| e.distill_partial_result(|_errors| {})) + .ok(); Ok((len, exif)) } diff --git a/src/blob/blob_tests.rs b/src/blob/blob_tests.rs index 68472f8679..a63c086027 100644 --- a/src/blob/blob_tests.rs +++ b/src/blob/blob_tests.rs @@ -334,6 +334,28 @@ async fn test_recode_image_2() { assert_correct_rotation(&img_rotated); } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_recode_image_bad_exif() { + // `exiftool` reports for this file "Bad offset for IFD0 XResolution", still Exif must be + // detected and removed. + let bytes = include_bytes!("../../test-data/image/1000x1000-bad-exif.jpg"); + SendImageCheckMediaquality { + viewtype: Viewtype::Image, + media_quality_config: "0", + bytes, + extension: "jpg", + has_exif: true, + original_width: 1000, + original_height: 1000, + compressed_width: 1000, + compressed_height: 1000, + ..Default::default() + } + .test() + .await + .unwrap(); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_recode_image_balanced_png() { let bytes = include_bytes!("../../test-data/image/screenshot.png"); @@ -418,7 +440,7 @@ async fn test_recode_image_balanced_png() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_sticker_with_exif() { - let bytes = include_bytes!("../../test-data/image/logo.png"); + let bytes = include_bytes!("../../test-data/image/logo-exif.png"); SendImageCheckMediaquality { viewtype: Viewtype::Sticker, bytes, diff --git a/src/calls.rs b/src/calls.rs index 2daf0e8eca..88cefb8e15 100644 --- a/src/calls.rs +++ b/src/calls.rs @@ -2,8 +2,9 @@ //! //! Internally, calls are bound a user-visible message initializing the call. //! This means, the "Call ID" is a "Message ID" - similar to Webxdc IDs. +use crate::chat::ChatIdBlocked; use crate::chat::{Chat, ChatId, send_msg}; -use crate::constants::Chattype; +use crate::constants::{Blocked, Chattype}; use crate::contact::ContactId; use crate::context::Context; use crate::events::EventType; @@ -345,12 +346,27 @@ impl Context { false } }; - self.emit_event(EventType::IncomingCall { - msg_id: call.msg.id, - chat_id: call.msg.chat_id, - place_call_info: call.place_call_info.to_string(), - has_video, - }); + if let Some(chat_id_blocked) = + ChatIdBlocked::lookup_by_contact(self, from_id).await? + { + match chat_id_blocked.blocked { + Blocked::Not => { + self.emit_event(EventType::IncomingCall { + msg_id: call.msg.id, + chat_id: call.msg.chat_id, + place_call_info: call.place_call_info.to_string(), + has_video, + }); + } + Blocked::Yes | Blocked::Request => { + // Do not notify about incoming calls + // from contact requests and blocked contacts. + // + // User can still access the call and accept it + // via the chat in case of contact requests. + } + } + } let wait = call.remaining_ring_seconds(); task::spawn(Context::emit_end_call_if_unaccepted( self.clone(), diff --git a/src/calls/calls_tests.rs b/src/calls/calls_tests.rs index 9ad40efeb1..3f983d8438 100644 --- a/src/calls/calls_tests.rs +++ b/src/calls/calls_tests.rs @@ -45,6 +45,12 @@ async fn setup_call() -> Result { // Alice creates a chat with Bob and places an outgoing call there. // Alice's other device sees the same message as an outgoing call. let alice_chat = alice.create_chat(&bob).await; + + // Create chat on Bob's side + // so incoming call causes a notification. + bob.create_chat(&alice).await; + bob2.create_chat(&alice).await; + let test_msg_id = alice .place_outgoing_call(alice_chat.id, PLACE_INFO.to_string()) .await?; diff --git a/src/chat.rs b/src/chat.rs index 796eaf32d4..28767e3966 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -12,7 +12,6 @@ use std::time::Duration; use anyhow::{Context as _, Result, anyhow, bail, ensure}; use chrono::TimeZone; use deltachat_contact_tools::{ContactAddress, sanitize_bidi_characters, sanitize_single_line}; -use deltachat_derive::{FromSql, ToSql}; use mail_builder::mime::MimePart; use serde::{Deserialize, Serialize}; use strum_macros::EnumIter; @@ -31,6 +30,7 @@ use crate::debug_logging::maybe_set_logging_xdc; use crate::download::DownloadState; use crate::ephemeral::{Timer as EphemeralTimer, start_chat_ephemeral_timers}; use crate::events::EventType; +use crate::key::self_fingerprint; use crate::location; use crate::log::{LogExt, error, info, warn}; use crate::logged_debug_assert; @@ -67,41 +67,6 @@ pub enum ChatItem { }, } -/// Chat protection status. -#[derive( - Debug, - Default, - Display, - Clone, - Copy, - PartialEq, - Eq, - FromPrimitive, - ToPrimitive, - FromSql, - ToSql, - IntoStaticStr, - Serialize, - Deserialize, -)] -#[repr(u32)] -pub enum ProtectionStatus { - /// Chat is not protected. - #[default] - Unprotected = 0, - - /// Chat is protected. - /// - /// All members of the chat must be verified. - Protected = 1, - // `2` was never used as a value. - - // Chats don't break in Core v2 anymore. Chats with broken protection existing before the - // key-contacts migration are treated as `Unprotected`. - // - // ProtectionBroken = 3, -} - /// The reason why messages cannot be sent to the chat. /// /// The reason is mainly for logging and displaying in debug REPL, thus not translated. @@ -306,14 +271,12 @@ impl ChatId { /// Create a group or mailinglist raw database record with the given parameters. /// The function does not add SELF nor checks if the record already exists. - #[expect(clippy::too_many_arguments)] pub(crate) async fn create_multiuser_record( context: &Context, chattype: Chattype, grpid: &str, grpname: &str, create_blocked: Blocked, - create_protected: ProtectionStatus, param: Option, timestamp: i64, ) -> Result { @@ -321,31 +284,27 @@ impl ChatId { let timestamp = cmp::min(timestamp, smeared_time(context)); let row_id = context.sql.insert( - "INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected, param) VALUES(?, ?, ?, ?, ?, ?, ?);", + "INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected, param) VALUES(?, ?, ?, ?, ?, 0, ?);", ( chattype, &grpname, grpid, create_blocked, timestamp, - create_protected, param.unwrap_or_default(), ), ).await?; let chat_id = ChatId::new(u32::try_from(row_id)?); + let chat = Chat::load_from_db(context, chat_id).await?; - if create_protected == ProtectionStatus::Protected { - chat_id - .add_protection_msg(context, ProtectionStatus::Protected, None, timestamp) - .await?; - } else { - chat_id.maybe_add_encrypted_msg(context, timestamp).await?; + if chat.is_encrypted(context).await? { + chat_id.add_encrypted_msg(context, timestamp).await?; } info!( context, - "Created group/mailinglist '{}' grpid={} as {}, blocked={}, protected={create_protected}.", + "Created group/mailinglist '{}' grpid={} as {}, blocked={}.", &grpname, grpid, chat_id, @@ -500,111 +459,8 @@ impl ChatId { Ok(()) } - /// Sets protection without sending a message. - /// - /// Returns whether the protection status was actually modified. - pub(crate) async fn inner_set_protection( - self, - context: &Context, - protect: ProtectionStatus, - ) -> Result { - ensure!(!self.is_special(), "Invalid chat-id {self}."); - - let chat = Chat::load_from_db(context, self).await?; - - if protect == chat.protected { - info!(context, "Protection status unchanged for {}.", self); - return Ok(false); - } - - match protect { - ProtectionStatus::Protected => match chat.typ { - Chattype::Single - | Chattype::Group - | Chattype::OutBroadcast - | Chattype::InBroadcast => {} - Chattype::Mailinglist => bail!("Cannot protect mailing lists"), - }, - ProtectionStatus::Unprotected => {} - }; - - context - .sql - .execute("UPDATE chats SET protected=? WHERE id=?;", (protect, self)) - .await?; - - context.emit_event(EventType::ChatModified(self)); - chatlist_events::emit_chatlist_item_changed(context, self); - - // make sure, the receivers will get all keys - self.reset_gossiped_timestamp(context).await?; - - Ok(true) - } - - /// Adds an info message to the chat, telling the user that the protection status changed. - /// - /// Params: - /// - /// * `contact_id`: In a 1:1 chat, pass the chat partner's contact id. - /// * `timestamp_sort` is used as the timestamp of the added message - /// and should be the timestamp of the change happening. - pub(crate) async fn add_protection_msg( - self, - context: &Context, - protect: ProtectionStatus, - contact_id: Option, - timestamp_sort: i64, - ) -> Result<()> { - if contact_id == Some(ContactId::SELF) { - // Do not add protection messages to Saved Messages chat. - // This chat never gets protected and unprotected, - // we do not want the first message - // to be a protection message with an arbitrary timestamp. - return Ok(()); - } - - let text = context.stock_protection_msg(protect, contact_id).await; - let cmd = match protect { - ProtectionStatus::Protected => SystemMessage::ChatProtectionEnabled, - ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled, - }; - add_info_msg_with_cmd( - context, - self, - &text, - cmd, - timestamp_sort, - None, - None, - None, - None, - ) - .await?; - - Ok(()) - } - - /// Adds message "Messages are end-to-end encrypted" if appropriate. - /// - /// This function is rather slow because it does a lot of database queries, - /// but this is fine because it is only called on chat creation. - async fn maybe_add_encrypted_msg(self, context: &Context, timestamp_sort: i64) -> Result<()> { - let chat = Chat::load_from_db(context, self).await?; - - // as secure-join adds its own message on success (after some other messasges), - // we do not want to add "Messages are end-to-end encrypted" on chat creation. - // we detect secure join by `can_send` (for Bob, scanner side) and by `blocked` (for Alice, inviter side) below. - if !chat.is_encrypted(context).await? - || self <= DC_CHAT_ID_LAST_SPECIAL - || chat.is_device_talk() - || chat.is_self_talk() - || (!chat.can_send(context).await? && !chat.is_contact_request()) - || chat.blocked == Blocked::Yes - { - return Ok(()); - } - + /// Adds message "Messages are end-to-end encrypted". + async fn add_encrypted_msg(self, context: &Context, timestamp_sort: i64) -> Result<()> { let text = stock_str::messages_e2e_encrypted(context).await; add_info_msg_with_cmd( context, @@ -621,74 +477,6 @@ impl ChatId { Ok(()) } - /// Sets protection and adds a message. - /// - /// `timestamp_sort` is used as the timestamp of the added message - /// and should be the timestamp of the change happening. - async fn set_protection_for_timestamp_sort( - self, - context: &Context, - protect: ProtectionStatus, - timestamp_sort: i64, - contact_id: Option, - ) -> Result<()> { - let protection_status_modified = self - .inner_set_protection(context, protect) - .await - .with_context(|| format!("Cannot set protection for {self}"))?; - if protection_status_modified { - self.add_protection_msg(context, protect, contact_id, timestamp_sort) - .await?; - chatlist_events::emit_chatlist_item_changed(context, self); - } - Ok(()) - } - - /// Sets protection and sends or adds a message. - /// - /// `timestamp_sent` is the "sent" timestamp of a message caused the protection state change. - pub(crate) async fn set_protection( - self, - context: &Context, - protect: ProtectionStatus, - timestamp_sent: i64, - contact_id: Option, - ) -> Result<()> { - let sort_to_bottom = true; - let (received, incoming) = (false, false); - let ts = self - .calc_sort_timestamp(context, timestamp_sent, sort_to_bottom, received, incoming) - .await? - // Always sort protection messages below `SystemMessage::SecurejoinWait{,Timeout}` ones - // in case of race conditions. - .saturating_add(1); - self.set_protection_for_timestamp_sort(context, protect, ts, contact_id) - .await - } - - /// Sets the 1:1 chat with the given address to ProtectionStatus::Protected, - /// and posts a `SystemMessage::ChatProtectionEnabled` into it. - /// - /// If necessary, creates a hidden chat for this. - pub(crate) async fn set_protection_for_contact( - context: &Context, - contact_id: ContactId, - timestamp: i64, - ) -> Result<()> { - let chat_id = ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Yes) - .await - .with_context(|| format!("can't create chat for {contact_id}"))?; - chat_id - .set_protection( - context, - ProtectionStatus::Protected, - timestamp, - Some(contact_id), - ) - .await?; - Ok(()) - } - /// Archives or unarchives a chat. pub async fn set_visibility(self, context: &Context, visibility: ChatVisibility) -> Result<()> { self.set_visibility_ex(context, Sync, visibility).await @@ -1396,16 +1184,6 @@ impl ChatId { Ok(()) } - /// Returns true if the chat is protected. - pub async fn is_protected(self, context: &Context) -> Result { - let protection_status = context - .sql - .query_get_value("SELECT protected FROM chats WHERE id=?", (self,)) - .await? - .unwrap_or_default(); - Ok(protection_status) - } - /// Returns the sort timestamp for a new message in the chat. /// /// `message_timestamp` should be either the message "sent" timestamp or a timestamp of the @@ -1560,9 +1338,6 @@ pub struct Chat { /// Duration of the chat being muted. pub mute_duration: MuteDuration, - - /// If the chat is protected (verified). - pub(crate) protected: ProtectionStatus, } impl Chat { @@ -1572,7 +1347,7 @@ impl Chat { .sql .query_row( "SELECT c.type, c.name, c.grpid, c.param, c.archived, - c.blocked, c.locations_send_until, c.muted_until, c.protected + c.blocked, c.locations_send_until, c.muted_until FROM chats c WHERE c.id=?;", (chat_id,), @@ -1587,7 +1362,6 @@ impl Chat { blocked: row.get::<_, Option<_>>(5)?.unwrap_or_default(), is_sending_locations: row.get(6)?, mute_duration: row.get(7)?, - protected: row.get(8)?, }; Ok(c) }, @@ -1868,53 +1642,38 @@ impl Chat { !self.is_unpromoted() } - /// Returns true if chat protection is enabled. - /// - /// UI should display a green checkmark - /// in the chat title, - /// in the chat profile title and - /// in the chatlist item - /// if chat protection is enabled. - /// UI should also display a green checkmark - /// in the contact profile - /// if 1:1 chat with this contact exists and is protected. - pub fn is_protected(&self) -> bool { - 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 => { - match context - .sql - .query_row_optional( - "SELECT cc.contact_id, c.fingerprint<>'' + let is_encrypted = match self.typ { + Chattype::Single => { + match context + .sql + .query_row_optional( + "SELECT cc.contact_id, c.fingerprint<>'' FROM chats_contacts cc LEFT JOIN contacts c ON c.id=cc.contact_id WHERE cc.chat_id=? ", - (self.id,), - |row| { - let id: ContactId = row.get(0)?; - let is_key: bool = row.get(1)?; - Ok((id, is_key)) - }, - ) - .await? - { - Some((id, is_key)) => is_key || id == ContactId::DEVICE, - None => true, - } - } - Chattype::Group => { - // Do not encrypt ad-hoc groups. - !self.grpid.is_empty() + (self.id,), + |row| { + let id: ContactId = row.get(0)?; + let is_key: bool = row.get(1)?; + Ok((id, is_key)) + }, + ) + .await? + { + Some((id, is_key)) => is_key || id == ContactId::DEVICE, + None => true, } - Chattype::Mailinglist => false, - Chattype::OutBroadcast | Chattype::InBroadcast => true, - }; + } + Chattype::Group => { + // Do not encrypt ad-hoc groups. + !self.grpid.is_empty() + } + Chattype::Mailinglist => false, + Chattype::OutBroadcast | Chattype::InBroadcast => true, + }; Ok(is_encrypted) } @@ -2248,17 +2007,21 @@ impl Chat { /// Sends a `SyncAction` synchronising chat contacts to other devices. pub(crate) async fn sync_contacts(&self, context: &Context) -> Result<()> { if self.is_encrypted(context).await? { + let self_fp = self_fingerprint(context).await?; let fingerprint_addrs = context .sql .query_map( - "SELECT c.fingerprint, c.addr + "SELECT c.id, 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)?; + if row.get::<_, ContactId>(0)? == ContactId::SELF { + return Ok((self_fp.to_string(), String::new())); + } + let fingerprint = row.get(1)?; + let addr = row.get(2)?; Ok((fingerprint, addr)) }, |addrs| addrs.collect::, _>>().map_err(Into::into), @@ -2625,7 +2388,6 @@ impl ChatIdBlocked { _ => (), } - let protected = contact_id == ContactId::SELF || contact.is_verified(context).await?; let smeared_time = create_smeared_timestamp(context); let chat_id = context @@ -2633,19 +2395,14 @@ impl ChatIdBlocked { .transaction(move |transaction| { transaction.execute( "INSERT INTO chats - (type, name, param, blocked, created_timestamp, protected) - VALUES(?, ?, ?, ?, ?, ?)", + (type, name, param, blocked, created_timestamp) + VALUES(?, ?, ?, ?, ?)", ( Chattype::Single, chat_name, params.to_string(), create_blocked as u8, smeared_time, - if protected { - ProtectionStatus::Protected - } else { - ProtectionStatus::Unprotected - }, ), )?; let chat_id = ChatId::new( @@ -2666,19 +2423,12 @@ impl ChatIdBlocked { }) .await?; - if protected { - chat_id - .add_protection_msg( - context, - ProtectionStatus::Protected, - Some(contact_id), - smeared_time, - ) - .await?; - } else { - chat_id - .maybe_add_encrypted_msg(context, smeared_time) - .await?; + let chat = Chat::load_from_db(context, chat_id).await?; + if chat.is_encrypted(context).await? + && !chat.param.exists(Param::Devicetalk) + && !chat.param.exists(Param::Selftalk) + { + chat_id.add_encrypted_msg(context, smeared_time).await?; } Ok(Self { @@ -3650,23 +3400,26 @@ pub async fn get_past_chat_contacts(context: &Context, chat_id: ChatId) -> Resul Ok(list) } -/// Creates a group chat with a given `name`. -/// Deprecated on 2025-06-21, use `create_group_ex()`. -pub async fn create_group_chat( - context: &Context, - protect: ProtectionStatus, - name: &str, -) -> Result { - create_group_ex(context, Some(protect), name).await +/// Creates an encrypted group chat. +pub async fn create_group(context: &Context, name: &str) -> Result { + create_group_ex(context, Sync, create_id(), name).await +} + +/// Creates an unencrypted group chat. +pub async fn create_group_unencrypted(context: &Context, name: &str) -> Result { + create_group_ex(context, Sync, String::new(), name).await } /// Creates a group chat. /// -/// * `encryption` - If `Some`, the chat is encrypted (with key-contacts) and can be protected. +/// * `sync` - Whether a multi-device synchronization message should be sent. Ignored for +/// unencrypted chats currently. +/// * `grpid` - Group ID. Iff nonempty, the chat is encrypted (with key-contacts). /// * `name` - Chat name. -pub async fn create_group_ex( +pub(crate) async fn create_group_ex( context: &Context, - encryption: Option, + sync: sync::Sync, + grpid: String, name: &str, ) -> Result { let mut chat_name = sanitize_single_line(name); @@ -3677,11 +3430,6 @@ pub async fn create_group_ex( chat_name = "…".to_string(); } - let grpid = match encryption { - Some(_) => create_id(), - None => String::new(), - }; - let timestamp = create_smeared_timestamp(context); let row_id = context .sql @@ -3689,7 +3437,7 @@ pub async fn create_group_ex( "INSERT INTO chats (type, name, grpid, param, created_timestamp) VALUES(?, ?, ?, \'U=1\', ?);", - (Chattype::Group, chat_name, grpid, timestamp), + (Chattype::Group, &chat_name, &grpid, timestamp), ) .await?; @@ -3700,19 +3448,9 @@ pub async fn create_group_ex( chatlist_events::emit_chatlist_changed(context); chatlist_events::emit_chatlist_item_changed(context, chat_id); - match encryption { - Some(ProtectionStatus::Protected) => { - let protect = ProtectionStatus::Protected; - chat_id - .set_protection_for_timestamp_sort(context, protect, timestamp, None) - .await?; - } - Some(ProtectionStatus::Unprotected) => { - // Add "Messages are end-to-end encrypted." message - // even to unprotected chats. - chat_id.maybe_add_encrypted_msg(context, timestamp).await?; - } - None => {} + if !grpid.is_empty() { + // Add "Messages are end-to-end encrypted." message. + chat_id.add_encrypted_msg(context, timestamp).await?; } if !context.get_config_bool(Config::Bot).await? @@ -3721,7 +3459,11 @@ pub async fn create_group_ex( let text = stock_str::new_group_send_first_message(context).await; add_info_msg(context, chat_id, &text, create_smeared_timestamp(context)).await?; } - + if let (true, true) = (sync.into(), !grpid.is_empty()) { + let id = SyncId::Grpid(grpid); + let action = SyncAction::CreateGroupEncrypted(chat_name); + self::sync(context, id, action).await.log_err(context).ok(); + } Ok(chat_id) } @@ -3737,7 +3479,7 @@ pub async fn create_group_ex( /// which would make it hard to grep for it. /// /// After creation, the chat contains no recipients and is in _unpromoted_ state; -/// see [`create_group_chat`] for more information on the unpromoted state. +/// see [`create_group`] for more information on the unpromoted state. /// /// Returns the created chat's id. pub async fn create_broadcast(context: &Context, chat_name: String) -> Result { @@ -3961,13 +3703,6 @@ pub(crate) async fn add_contact_to_chat_ex( } } else { // else continue and send status mail - if chat.is_protected() && !contact.is_verified(context).await? { - error!( - context, - "Cannot add non-bidirectionally verified contact {contact_id} to protected chat {chat_id}." - ); - return Ok(false); - } if is_contact_in_chat(context, chat_id, contact_id).await? { return Ok(false); } @@ -4627,24 +4362,21 @@ pub(crate) async fn get_chat_cnt(context: &Context) -> Result { } } -/// Returns a tuple of `(chatid, is_protected, blocked)`. +/// Returns a tuple of `(chatid, blocked)`. pub(crate) async fn get_chat_id_by_grpid( context: &Context, grpid: &str, -) -> Result> { +) -> Result> { context .sql .query_row_optional( - "SELECT id, blocked, protected FROM chats WHERE grpid=?;", + "SELECT id, blocked FROM chats WHERE grpid=?;", (grpid,), |row| { let chat_id = row.get::<_, ChatId>(0)?; let b = row.get::<_, Option>(1)?.unwrap_or_default(); - let p = row - .get::<_, Option>(2)? - .unwrap_or_default(); - Ok((chat_id, p == ProtectionStatus::Protected, b)) + Ok((chat_id, b)) }, ) .await @@ -4947,16 +4679,14 @@ async fn set_contacts_by_fingerprints( "Cannot add key-contacts to unencrypted chat {id}" ); ensure!( - chat.typ == Chattype::OutBroadcast, - "{id} is not a broadcast list", + matches!(chat.typ, Chattype::Group | Chattype::OutBroadcast), + "{id} is not a group or broadcast", ); 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, "", addr, fingerprint, Origin::Hidden) + .await? + .0; contacts.insert(contact); } let contacts_old = HashSet::::from_iter(get_chat_contacts(context, id).await?); @@ -4995,7 +4725,7 @@ pub(crate) enum SyncId { /// "Message-ID"-s, from oldest to latest. Used for ad-hoc groups. Msgids(Vec), - // Special id for device chat. + /// Special id for device chat. Device, } @@ -5009,6 +4739,8 @@ pub(crate) enum SyncAction { SetMuted(MuteDuration), /// Create broadcast channel with the given name. CreateBroadcast(String), + /// Create encrypted group chat with the given name. + CreateGroupEncrypted(String), Rename(String), /// Set chat contacts by their addresses. SetContacts(Vec), @@ -5074,6 +4806,9 @@ impl Context { if let SyncAction::CreateBroadcast(name) = action { create_broadcast_ex(self, Nosync, grpid.clone(), name.clone()).await?; return Ok(()); + } else if let SyncAction::CreateGroupEncrypted(name) = action { + create_group_ex(self, Nosync, grpid.clone(), name).await?; + return Ok(()); } get_chat_id_by_grpid(self, grpid) .await? @@ -5095,7 +4830,7 @@ impl Context { SyncAction::Accept => chat_id.accept_ex(self, Nosync).await, SyncAction::SetVisibility(v) => chat_id.set_visibility_ex(self, Nosync, *v).await, SyncAction::SetMuted(duration) => set_muted_ex(self, Nosync, chat_id, *duration).await, - SyncAction::CreateBroadcast(_) => { + SyncAction::CreateBroadcast(_) | SyncAction::CreateGroupEncrypted(..) => { Err(anyhow!("sync_alter_chat({id:?}, {action:?}): Bad request.")) } SyncAction::Rename(to) => rename_ex(self, Nosync, chat_id, to).await, diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 216c71efd9..45c228834a 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -96,7 +96,7 @@ async fn test_get_draft() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_delete_draft() -> Result<()> { let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?; + let chat_id = create_group(&t, "abc").await?; let mut msg = Message::new_text("hi!".to_string()); chat_id.set_draft(&t, Some(&mut msg)).await?; @@ -120,7 +120,7 @@ async fn test_forwarding_draft_failing() -> Result<()> { chat_id.set_draft(&t, Some(&mut msg)).await?; assert_eq!(msg.id, chat_id.get_draft(&t).await?.unwrap().id); - let chat_id2 = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let chat_id2 = create_group(&t, "foo").await?; assert!(forward_msgs(&t, &[msg.id], chat_id2).await.is_err()); Ok(()) } @@ -169,7 +169,7 @@ async fn test_draft_stable_ids() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_only_one_draft_per_chat() -> Result<()> { let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?; + let chat_id = create_group(&t, "abc").await?; let msgs: Vec = (1..=1000) .map(|i| Message::new_text(i.to_string())) @@ -196,7 +196,7 @@ async fn test_only_one_draft_per_chat() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_change_quotes_on_reused_message_object() -> Result<()> { let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?; + let chat_id = create_group(&t, "chat").await?; let quote1 = Message::load_from_db(&t, send_text_msg(&t, chat_id, "quote1".to_string()).await?).await?; let quote2 = @@ -247,7 +247,7 @@ async fn test_quote_replies() -> Result<()> { let alice = TestContext::new_alice().await; let bob = TestContext::new_bob().await; - let grp_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?; + let grp_chat_id = create_group(&alice, "grp").await?; let grp_msg_id = send_text_msg(&alice, grp_chat_id, "bar".to_string()).await?; let grp_msg = Message::load_from_db(&alice, grp_msg_id).await?; @@ -295,9 +295,7 @@ async fn test_quote_replies() -> Result<()> { async fn test_add_contact_to_chat_ex_add_self() { // Adding self to a contact should succeed, even though it's pointless. let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo") - .await - .unwrap(); + let chat_id = create_group(&t, "foo").await.unwrap(); let added = add_contact_to_chat_ex(&t, Nosync, chat_id, ContactId::SELF, false) .await .unwrap(); @@ -336,8 +334,7 @@ async fn test_member_add_remove() -> Result<()> { } tcm.section("Create and promote a group."); - let alice_chat_id = - create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?; + let alice_chat_id = create_group(&alice, "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?; let sent = alice @@ -399,8 +396,7 @@ async fn test_parallel_member_remove() -> Result<()> { let alice_charlie_contact_id = alice.add_or_lookup_contact_id(&charlie).await; tcm.section("Alice creates and promotes a group"); - let alice_chat_id = - create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?; + let alice_chat_id = create_group(&alice, "Group chat").await?; add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?; add_contact_to_chat(&alice, alice_chat_id, alice_fiona_contact_id).await?; let alice_sent_msg = alice @@ -457,8 +453,7 @@ async fn test_msg_with_implicit_member_removed() -> Result<()> { let alice_bob_contact_id = alice.add_or_lookup_contact_id(&bob).await; let alice_fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await; let bob_fiona_contact_id = bob.add_or_lookup_contact_id(&fiona).await; - let alice_chat_id = - create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?; + let alice_chat_id = create_group(&alice, "Group chat").await?; add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?; let sent_msg = alice.send_text(alice_chat_id, "I created a group").await; let bob_received_msg = bob.recv_msg(&sent_msg).await; @@ -488,7 +483,7 @@ async fn test_msg_with_implicit_member_removed() -> Result<()> { // If Bob sends a message to Alice now, Fiona is removed. assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 3); let sent_msg = bob - .send_text(alice_chat_id, "I have removed Fiona some time ago.") + .send_text(bob_chat_id, "I have removed Fiona some time ago.") .await; alice.recv_msg(&sent_msg).await; assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2); @@ -504,7 +499,7 @@ async fn test_modify_chat_multi_device() -> Result<()> { a1.set_config_bool(Config::BccSelf, true).await?; // create group and sync it to the second device - let a1_chat_id = create_group_chat(&a1, ProtectionStatus::Unprotected, "foo").await?; + let a1_chat_id = create_group(&a1, "foo").await?; let sent = a1.send_text(a1_chat_id, "ho!").await; let a1_msg = a1.get_last_msg().await; let a1_chat = Chat::load_from_db(&a1, a1_chat_id).await?; @@ -602,7 +597,7 @@ async fn test_modify_chat_disordered() -> Result<()> { let fiona = tcm.fiona().await; let fiona_id = alice.add_or_lookup_contact_id(&fiona).await; - let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?; + let alice_chat_id = create_group(&alice, "foo").await?; send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?; add_contact_to_chat(&alice, alice_chat_id, bob_id).await?; @@ -649,9 +644,7 @@ async fn test_lost_member_added() -> Result<()> { 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; + let alice_chat_id = alice.create_group_with_members("Group", &[bob]).await; let alice_sent = alice.send_text(alice_chat_id, "Hi!").await; let bob_chat_id = bob.recv_msg(&alice_sent).await.chat_id; assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 2); @@ -681,7 +674,7 @@ async fn test_modify_chat_lost() -> Result<()> { let fiona = tcm.fiona().await; let fiona_id = alice.add_or_lookup_contact_id(&fiona).await; - let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?; + let alice_chat_id = create_group(&alice, "foo").await?; add_contact_to_chat(&alice, alice_chat_id, bob_id).await?; add_contact_to_chat(&alice, alice_chat_id, charlie_id).await?; add_contact_to_chat(&alice, alice_chat_id, fiona_id).await?; @@ -722,7 +715,7 @@ async fn test_leave_group() -> Result<()> { let bob = tcm.bob().await; tcm.section("Alice creates group chat with Bob."); - let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?; + let alice_chat_id = create_group(&alice, "foo").await?; let bob_contact = alice.add_or_lookup_contact(&bob).await.id; add_contact_to_chat(&alice, alice_chat_id, bob_contact).await?; @@ -1381,9 +1374,7 @@ async fn test_pinned() { tokio::time::sleep(std::time::Duration::from_millis(1000)).await; let chat_id2 = t.get_self_chat().await.id; tokio::time::sleep(std::time::Duration::from_millis(1000)).await; - let chat_id3 = create_group_chat(&t, ProtectionStatus::Unprotected, "foo") - .await - .unwrap(); + let chat_id3 = create_group(&t, "foo").await.unwrap(); let chatlist = get_chats_from_chat_list(&t, DC_GCL_NO_SPECIALS).await; assert_eq!(chatlist, vec![chat_id3, chat_id2, chat_id1]); @@ -1473,9 +1464,7 @@ async fn test_set_chat_name() { let mut tcm = TestContextManager::new(); let alice = &tcm.alice().await; - let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foo") - .await - .unwrap(); + let chat_id = create_group(alice, "foo").await.unwrap(); assert_eq!( Chat::load_from_db(alice, chat_id).await.unwrap().get_name(), "foo" @@ -1547,7 +1536,7 @@ async fn test_shall_attach_selfavatar() -> Result<()> { let alice = &tcm.alice().await; let bob = &tcm.bob().await; - let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foo").await?; + let chat_id = create_group(alice, "foo").await?; assert!(!shall_attach_selfavatar(alice, chat_id).await?); let contact_id = alice.add_or_lookup_contact_id(bob).await; @@ -1569,7 +1558,7 @@ 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 chat_id = create_group(t, "foo").await?; let contact_id = t.add_or_lookup_contact_id(bob).await; add_contact_to_chat(t, chat_id, contact_id).await?; @@ -1594,9 +1583,7 @@ async fn test_profile_data_on_group_leave() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_set_mute_duration() { let t = TestContext::new().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo") - .await - .unwrap(); + let chat_id = create_group(&t, "foo").await.unwrap(); // Initial assert_eq!( Chat::load_from_db(&t, chat_id).await.unwrap().is_muted(), @@ -1645,7 +1632,7 @@ async fn test_set_mute_duration() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_add_info_msg() -> Result<()> { let t = TestContext::new().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let chat_id = create_group(&t, "foo").await?; add_info_msg(&t, chat_id, "foo info", time()).await?; let msg = t.get_last_msg_in(chat_id).await; @@ -1662,7 +1649,7 @@ async fn test_add_info_msg() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_add_info_msg_with_cmd() -> Result<()> { let t = TestContext::new().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let chat_id = create_group(&t, "foo").await?; let msg_id = add_info_msg_with_cmd( &t, chat_id, @@ -1931,14 +1918,14 @@ async fn test_classic_email_chat() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_chat_get_color() -> Result<()> { let t = TestContext::new().await; - let chat_id = create_group_ex(&t, None, "a chat").await?; + let chat_id = create_group_unencrypted(&t, "a chat").await?; let color1 = Chat::load_from_db(&t, chat_id).await?.get_color(&t).await?; assert_eq!(color1, 0x6239dc); // upper-/lowercase makes a difference for the colors, these are different groups // (in contrast to email addresses, where upper-/lowercase is ignored in practise) let t = TestContext::new().await; - let chat_id = create_group_ex(&t, None, "A CHAT").await?; + let chat_id = create_group_unencrypted(&t, "A CHAT").await?; let color2 = Chat::load_from_db(&t, chat_id).await?.get_color(&t).await?; assert_ne!(color2, color1); Ok(()) @@ -1948,7 +1935,7 @@ async fn test_chat_get_color() -> Result<()> { async fn test_chat_get_color_encrypted() -> Result<()> { let mut tcm = TestContextManager::new(); let t = &tcm.alice().await; - let chat_id = create_group_ex(t, Some(ProtectionStatus::Unprotected), "a chat").await?; + let chat_id = create_group(t, "a chat").await?; let color1 = Chat::load_from_db(t, chat_id).await?.get_color(t).await?; set_chat_name(t, chat_id, "A CHAT").await?; let color2 = Chat::load_from_db(t, chat_id).await?.get_color(t).await?; @@ -2137,7 +2124,7 @@ async fn test_forward_info_msg() -> Result<()> { let alice = &tcm.alice().await; let bob = &tcm.bob().await; - let chat_id1 = create_group_chat(alice, ProtectionStatus::Unprotected, "a").await?; + let chat_id1 = create_group(alice, "a").await?; send_text_msg(alice, chat_id1, "msg one".to_string()).await?; let bob_id = alice.add_or_lookup_contact_id(bob).await; add_contact_to_chat(alice, chat_id1, bob_id).await?; @@ -2204,8 +2191,7 @@ async fn test_forward_group() -> Result<()> { let bob_chat = bob.create_chat(&alice).await; // Alice creates a group with Bob. - let alice_group_chat_id = - create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?; + let alice_group_chat_id = create_group(&alice, "Group").await?; let bob_id = alice.add_or_lookup_contact_id(&bob).await; let charlie_id = alice.add_or_lookup_contact_id(&charlie).await; add_contact_to_chat(&alice, alice_group_chat_id, bob_id).await?; @@ -2257,8 +2243,7 @@ async fn test_only_minimal_data_are_forwarded() -> Result<()> { .set_config(Config::Displayname, Some("secretname")) .await?; let bob_id = alice.add_or_lookup_contact_id(&bob).await; - let group_id = - create_group_chat(&alice, ProtectionStatus::Unprotected, "secretgrpname").await?; + let group_id = create_group(&alice, "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; @@ -2273,7 +2258,7 @@ async fn test_only_minimal_data_are_forwarded() -> Result<()> { let orig_msg = bob.recv_msg(&sent_msg).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?; + let group_id = create_group(&bob, "group2").await?; add_contact_to_chat(&bob, group_id, charlie_id).await?; let broadcast_id = create_broadcast(&bob, "Channel".to_string()).await?; add_contact_to_chat(&bob, broadcast_id, charlie_id).await?; @@ -2371,7 +2356,7 @@ async fn test_save_msgs_order() -> Result<()> { for a in [alice, alice1] { a.set_config_bool(Config::SyncMsgs, true).await?; } - let chat_id = create_group_chat(alice, ProtectionStatus::Protected, "grp").await?; + let chat_id = create_group(alice, "grp").await?; let sent = [ alice.send_text(chat_id, "0").await, alice.send_text(chat_id, "1").await, @@ -2427,14 +2412,14 @@ async fn test_forward_from_saved_to_saved() -> Result<()> { let bob = TestContext::new_bob().await; let sent = alice.send_text(alice.create_chat(&bob).await.id, "k").await; - bob.recv_msg(&sent).await; + let received_message = bob.recv_msg(&sent).await; let orig = bob.get_last_msg().await; let self_chat = bob.get_self_chat().await; save_msgs(&bob, &[orig.id]).await?; let saved1 = bob.get_last_msg().await; assert_eq!( saved1.get_original_msg_id(&bob).await?.unwrap(), - sent.sender_msg_id + received_message.id ); assert_ne!(saved1.from_id, ContactId::SELF); @@ -2492,7 +2477,7 @@ async fn test_resend_own_message() -> Result<()> { let alice = TestContext::new_alice().await; let bob = TestContext::new_bob().await; let fiona = TestContext::new_fiona().await; - let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?; + let alice_grp = create_group(&alice, "grp").await?; add_contact_to_chat( &alice, alice_grp, @@ -2579,7 +2564,7 @@ async fn test_resend_foreign_message_fails() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = &tcm.alice().await; let bob = &tcm.bob().await; - let alice_grp = create_group_chat(alice, ProtectionStatus::Unprotected, "grp").await?; + let alice_grp = create_group(alice, "grp").await?; add_contact_to_chat(alice, alice_grp, alice.add_or_lookup_contact_id(bob).await).await?; let sent1 = alice.send_text(alice_grp, "alice->bob").await; @@ -2596,7 +2581,7 @@ async fn test_resend_info_message_fails() -> Result<()> { let bob = &tcm.bob().await; let charlie = &tcm.charlie().await; - let alice_grp = create_group_chat(alice, ProtectionStatus::Unprotected, "grp").await?; + let alice_grp = create_group(alice, "grp").await?; add_contact_to_chat(alice, alice_grp, alice.add_or_lookup_contact_id(bob).await).await?; alice.send_text(alice_grp, "alice->bob").await; @@ -2619,7 +2604,7 @@ async fn test_can_send_group() -> Result<()> { let chat_id = ChatId::create_for_contact(&alice, bob).await?; let chat = Chat::load_from_db(&alice, chat_id).await?; assert!(chat.can_send(&alice).await?); - let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?; + let chat_id = create_group(&alice, "foo").await?; assert_eq!( Chat::load_from_db(&alice, chat_id) .await? @@ -2659,7 +2644,7 @@ async fn test_broadcast() -> Result<()> { add_contact_to_chat( &alice, broadcast_id, - get_chat_contacts(&alice, chat_bob.id).await?.pop().unwrap(), + get_chat_contacts(&alice, msg.chat_id).await?.pop().unwrap(), ) .await?; let fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await; @@ -3115,7 +3100,7 @@ async fn test_chat_get_encryption_info() -> Result<()> { 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?; + let chat_id = create_group(alice, "Group").await?; assert_eq!( chat_id.get_encryption_info(alice).await?, "End-to-end encryption available" @@ -3177,9 +3162,7 @@ async fn test_out_failed_on_all_keys_missing() -> Result<()> { let bob = &tcm.bob().await; let fiona = &tcm.fiona().await; - let bob_chat_id = bob - .create_group_with_members(ProtectionStatus::Unprotected, "", &[alice, fiona]) - .await; + let bob_chat_id = bob.create_group_with_members("", &[alice, fiona]).await; bob.send_text(bob_chat_id, "Gossiping Fiona's key").await; alice .recv_msg(&bob.send_text(bob_chat_id, "No key gossip").await) @@ -3197,8 +3180,8 @@ async fn test_out_failed_on_all_keys_missing() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_get_chat_media() -> Result<()> { let t = TestContext::new_alice().await; - let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; - let chat_id2 = create_group_chat(&t, ProtectionStatus::Unprotected, "bar").await?; + let chat_id1 = create_group(&t, "foo").await?; + let chat_id2 = create_group(&t, "bar").await?; assert_eq!( get_chat_media( @@ -3422,7 +3405,7 @@ async fn test_get_chat_media_webxdc_order() -> Result<()> { async fn test_blob_renaming() -> Result<()> { let alice = TestContext::new_alice().await; let bob = TestContext::new_bob().await; - let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?; + let chat_id = create_group(&alice, "Group").await?; add_contact_to_chat(&alice, chat_id, alice.add_or_lookup_contact_id(&bob).await).await?; let file = alice.get_blobdir().join("harmless_file.\u{202e}txt.exe"); fs::write(&file, "aaa").await?; @@ -3484,9 +3467,7 @@ async fn test_sync_blocked() -> Result<()> { // - Group chats synchronisation. // - That blocking a group deletes it on other devices. let fiona = TestContext::new_fiona().await; - let fiona_grp_chat_id = fiona - .create_group_with_members(ProtectionStatus::Unprotected, "grp", &[alice0]) - .await; + let fiona_grp_chat_id = fiona.create_group_with_members("grp", &[alice0]).await; let sent_msg = fiona.send_text(fiona_grp_chat_id, "hi").await; let a0_grp_chat_id = alice0.recv_msg(&sent_msg).await.chat_id; let a1_grp_chat_id = alice1.recv_msg(&sent_msg).await.chat_id; @@ -3619,9 +3600,7 @@ async fn test_sync_delete_chat() -> Result<()> { .get_matching(|evt| matches!(evt, EventType::ChatDeleted { .. })) .await; - let bob_grp_chat_id = bob - .create_group_with_members(ProtectionStatus::Unprotected, "grp", &[alice0]) - .await; + let bob_grp_chat_id = bob.create_group_with_members("grp", &[alice0]).await; let sent_msg = bob.send_text(bob_grp_chat_id, "hi").await; let a0_grp_chat_id = alice0.recv_msg(&sent_msg).await.chat_id; let a1_grp_chat_id = alice1.recv_msg(&sent_msg).await.chat_id; @@ -3854,6 +3833,61 @@ async fn test_sync_name() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_sync_create_group() -> Result<()> { + 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 = &tcm.bob().await; + let a0_bob_contact_id = alice0.add_or_lookup_contact_id(bob).await; + let a1_bob_contact_id = alice1.add_or_lookup_contact_id(bob).await; + let a0_chat_id = create_group(alice0, "grp").await?; + sync(alice0, alice1).await; + let a0_chat = Chat::load_from_db(alice0, a0_chat_id).await?; + let a1_chat_id = get_chat_id_by_grpid(alice1, &a0_chat.grpid) + .await? + .unwrap() + .0; + let a1_chat = Chat::load_from_db(alice1, a1_chat_id).await?; + assert_eq!(a1_chat.get_type(), Chattype::Group); + assert_eq!(a1_chat.is_promoted(), false); + assert_eq!(a1_chat.get_name(), "grp"); + + set_chat_name(alice0, a0_chat_id, "renamed").await?; + sync(alice0, alice1).await; + let a1_chat = Chat::load_from_db(alice1, a1_chat_id).await?; + assert_eq!(a1_chat.is_promoted(), false); + assert_eq!(a1_chat.get_name(), "renamed"); + + add_contact_to_chat(alice0, a0_chat_id, a0_bob_contact_id).await?; + sync(alice0, alice1).await; + let a1_chat = Chat::load_from_db(alice1, a1_chat_id).await?; + assert_eq!(a1_chat.is_promoted(), false); + assert_eq!( + get_chat_contacts(alice1, a1_chat_id).await?, + [a1_bob_contact_id, ContactId::SELF] + ); + + // Let's test a contact removal from another device. + remove_contact_from_chat(alice1, a1_chat_id, a1_bob_contact_id).await?; + sync(alice1, alice0).await; + let a0_chat = Chat::load_from_db(alice0, a0_chat_id).await?; + assert_eq!(a0_chat.is_promoted(), false); + assert_eq!( + get_chat_contacts(alice0, a0_chat_id).await?, + [ContactId::SELF] + ); + + let sent_msg = alice0.send_text(a0_chat_id, "hi").await; + let msg = alice1.recv_msg(&sent_msg).await; + assert_eq!(msg.chat_id, a1_chat_id); + assert_eq!(a1_chat_id.is_promoted(alice1).await?, true); + Ok(()) +} + /// Tests sending JPEG image with .png extension. /// /// This is a regression test, previously sending failed @@ -3988,9 +4022,7 @@ async fn test_info_contact_id() -> Result<()> { } // Alice creates group, Bob receives group - let alice_chat_id = alice - .create_group_with_members(ProtectionStatus::Unprotected, "play", &[bob]) - .await; + let alice_chat_id = alice.create_group_with_members("play", &[bob]).await; let sent_msg1 = alice.send_text(alice_chat_id, "moin").await; let msg = bob.recv_msg(&sent_msg1).await; @@ -4036,26 +4068,27 @@ async fn test_info_contact_id() -> Result<()> { ) .await?; - let fiona_id = alice.add_or_lookup_contact_id(&tcm.fiona().await).await; // contexts are in sync, fiona_id is same everywhere - add_contact_to_chat(alice, alice_chat_id, fiona_id).await?; + let alice_fiona_id = alice.add_or_lookup_contact_id(&tcm.fiona().await).await; + let bob_fiona_id = bob.add_or_lookup_contact_id(&tcm.fiona().await).await; + add_contact_to_chat(alice, alice_chat_id, alice_fiona_id).await?; pop_recv_and_check( alice, alice2, bob, SystemMessage::MemberAddedToGroup, - fiona_id, - fiona_id, + alice_fiona_id, + bob_fiona_id, ) .await?; - remove_contact_from_chat(alice, alice_chat_id, fiona_id).await?; + remove_contact_from_chat(alice, alice_chat_id, alice_fiona_id).await?; pop_recv_and_check( alice, alice2, bob, SystemMessage::MemberRemovedFromGroup, - fiona_id, - fiona_id, + alice_fiona_id, + bob_fiona_id, ) .await?; @@ -4063,11 +4096,12 @@ async fn test_info_contact_id() -> Result<()> { // We raw delete in db as Contact::delete() leaves a tombstone (which is great as the tap works longer then) alice .sql - .execute("DELETE FROM contacts WHERE id=?", (fiona_id,)) + .execute("DELETE FROM contacts WHERE id=?", (alice_fiona_id,)) .await?; let msg = alice.get_last_msg().await; assert_eq!(msg.get_info_type(), SystemMessage::MemberRemovedFromGroup); assert!(msg.get_info_contact_id(alice).await?.is_none()); + assert!(msg.get_info_contact_id(bob).await?.is_none()); Ok(()) } @@ -4085,8 +4119,7 @@ async fn test_add_member_bug() -> Result<()> { let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await; // Create a group. - let alice_chat_id = - create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?; + let alice_chat_id = create_group(alice, "Group chat").await?; add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?; add_contact_to_chat(alice, alice_chat_id, alice_fiona_contact_id).await?; @@ -4130,8 +4163,7 @@ async fn test_past_members() -> Result<()> { 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?; + let alice_chat_id = create_group(alice, "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.") @@ -4165,8 +4197,7 @@ async fn test_non_member_cannot_modify_member_list() -> Result<()> { let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await; - let alice_chat_id = - create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?; + let alice_chat_id = create_group(alice, "Group chat").await?; add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?; let alice_sent_msg = alice .send_text(alice_chat_id, "Hi! I created a group.") @@ -4209,8 +4240,7 @@ async fn unpromoted_group_no_tombstones() -> Result<()> { let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await; let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await; - let alice_chat_id = - create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?; + let alice_chat_id = create_group(alice, "Group chat").await?; add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?; add_contact_to_chat(alice, alice_chat_id, alice_fiona_contact_id).await?; assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 3); @@ -4241,8 +4271,7 @@ async fn test_expire_past_members_after_60_days() -> Result<()> { let fiona = &tcm.fiona().await; let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await; - let alice_chat_id = - create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?; + let alice_chat_id = create_group(alice, "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.") @@ -4279,7 +4308,7 @@ async fn test_past_members_order() -> Result<()> { let fiona = tcm.fiona().await; let fiona_contact_id = t.add_or_lookup_contact_id(&fiona).await; - let chat_id = create_group_chat(t, ProtectionStatus::Unprotected, "Group chat").await?; + let chat_id = create_group(t, "Group chat").await?; add_contact_to_chat(t, chat_id, bob_contact_id).await?; add_contact_to_chat(t, chat_id, charlie_contact_id).await?; add_contact_to_chat(t, chat_id, fiona_contact_id).await?; @@ -4341,8 +4370,7 @@ async fn test_restore_backup_after_60_days() -> Result<()> { let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await; let alice_charlie_contact_id = alice.add_or_lookup_contact_id(charlie).await; - let alice_chat_id = - create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?; + let alice_chat_id = create_group(alice, "Group chat").await?; add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?; add_contact_to_chat(alice, alice_chat_id, alice_charlie_contact_id).await?; @@ -4530,9 +4558,7 @@ async fn test_cannot_send_edit_request() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = &tcm.alice().await; let bob = &tcm.bob().await; - let chat_id = alice - .create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[bob]) - .await; + let chat_id = alice.create_group_with_members("My Group", &[bob]).await; // Alice can edit her message let sent1 = alice.send_text(chat_id, "foo").await; @@ -4703,7 +4729,7 @@ async fn test_no_address_contacts_in_group_chats() -> Result<()> { let bob = &tcm.bob().await; let charlie = &tcm.charlie().await; - let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?; + let chat_id = create_group(alice, "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_address_contact_id(charlie).await; @@ -4762,7 +4788,7 @@ async fn test_create_unencrypted_group_chat() -> Result<()> { let bob = &tcm.bob().await; let charlie = &tcm.charlie().await; - let chat_id = create_group_ex(alice, None, "Group chat").await?; + let chat_id = create_group_unencrypted(alice, "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_address_contact_id(charlie).await; @@ -4783,7 +4809,7 @@ async fn test_create_unencrypted_group_chat() -> Result<()> { async fn test_create_group_invalid_name() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = &tcm.alice().await; - let chat_id = create_group_ex(alice, None, " ").await?; + let chat_id = create_group(alice, " ").await?; let chat = Chat::load_from_db(alice, chat_id).await?; assert_eq!(chat.get_name(), "…"); Ok(()) @@ -4829,7 +4855,7 @@ async fn test_long_group_name() -> Result<()> { let bob = &tcm.bob().await; let group_name = "δδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδ"; - let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, group_name).await?; + let alice_chat_id = create_group(alice, group_name).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?; let sent = alice diff --git a/src/chatlist.rs b/src/chatlist.rs index 4e3090c5c2..81734a869f 100644 --- a/src/chatlist.rs +++ b/src/chatlist.rs @@ -481,8 +481,8 @@ mod tests { use super::*; use crate::chat::save_msgs; use crate::chat::{ - ProtectionStatus, add_contact_to_chat, create_group_chat, get_chat_contacts, - remove_contact_from_chat, send_text_msg, + add_contact_to_chat, create_group, get_chat_contacts, remove_contact_from_chat, + send_text_msg, }; use crate::receive_imf::receive_imf; use crate::stock_str::StockMessage; @@ -495,15 +495,9 @@ mod tests { async fn test_try_load() { let mut tcm = TestContextManager::new(); let bob = &tcm.bob().await; - let chat_id1 = create_group_chat(bob, ProtectionStatus::Unprotected, "a chat") - .await - .unwrap(); - let chat_id2 = create_group_chat(bob, ProtectionStatus::Unprotected, "b chat") - .await - .unwrap(); - let chat_id3 = create_group_chat(bob, ProtectionStatus::Unprotected, "c chat") - .await - .unwrap(); + let chat_id1 = create_group(bob, "a chat").await.unwrap(); + let chat_id2 = create_group(bob, "b chat").await.unwrap(); + let chat_id3 = create_group(bob, "c chat").await.unwrap(); // check that the chatlist starts with the most recent message let chats = Chatlist::try_load(bob, 0, None, None).await.unwrap(); @@ -536,9 +530,7 @@ mod tests { // receive a message from alice let alice = &tcm.alice().await; - let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "alice chat") - .await - .unwrap(); + let alice_chat_id = create_group(alice, "alice chat").await.unwrap(); add_contact_to_chat( alice, alice_chat_id, @@ -576,9 +568,7 @@ mod tests { async fn test_sort_self_talk_up_on_forward() { let t = TestContext::new_alice().await; t.update_device_chats().await.unwrap(); - create_group_chat(&t, ProtectionStatus::Unprotected, "a chat") - .await - .unwrap(); + create_group(&t, "a chat").await.unwrap(); let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 3); @@ -765,9 +755,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_get_summary_unwrap() { let t = TestContext::new().await; - let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat") - .await - .unwrap(); + let chat_id1 = create_group(&t, "a chat").await.unwrap(); let mut msg = Message::new_text("foo:\nbar \r\n test".to_string()); chat_id1.set_draft(&t, Some(&mut msg)).await.unwrap(); @@ -783,9 +771,7 @@ mod tests { async fn test_get_summary_deleted_draft() { let t = TestContext::new().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat") - .await - .unwrap(); + let chat_id = create_group(&t, "a chat").await.unwrap(); let mut msg = Message::new_text("Foobar".to_string()); chat_id.set_draft(&t, Some(&mut msg)).await.unwrap(); @@ -824,15 +810,9 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_load_broken() { let t = TestContext::new_bob().await; - let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat") - .await - .unwrap(); - create_group_chat(&t, ProtectionStatus::Unprotected, "b chat") - .await - .unwrap(); - create_group_chat(&t, ProtectionStatus::Unprotected, "c chat") - .await - .unwrap(); + let chat_id1 = create_group(&t, "a chat").await.unwrap(); + create_group(&t, "b chat").await.unwrap(); + create_group(&t, "c chat").await.unwrap(); // check that the chatlist starts with the most recent message let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); diff --git a/src/config.rs b/src/config.rs index 9bc13d317c..1f73262b2c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -389,12 +389,6 @@ pub enum Config { /// Make all outgoing messages with Autocrypt header "multipart/signed". SignUnencrypted, - /// Enable header protection for `Autocrypt` header. - /// - /// This is an experimental setting not compatible to other MUAs - /// and older Delta Chat versions (core version <= v1.149.0). - ProtectAutocrypt, - /// Let the core save all events to the database. /// This value is used internally to remember the MsgId of the logging xdc #[strum(props(default = "0"))] diff --git a/src/configure.rs b/src/configure.rs index 31d81200b9..91aff2c5c0 100644 --- a/src/configure.rs +++ b/src/configure.rs @@ -300,8 +300,6 @@ async fn get_configured_param( param.smtp.password.clone() }; - let proxy_enabled = ctx.get_config_bool(Config::ProxyEnabled).await?; - let mut addr = param.addr.clone(); if param.oauth2 { // the used oauth2 addr may differ, check this. @@ -343,7 +341,7 @@ async fn get_configured_param( "checking internal provider-info for offline autoconfig" ); - provider = provider::get_provider_info(ctx, ¶m_domain, proxy_enabled).await; + provider = provider::get_provider_info(¶m_domain); if let Some(provider) = provider { if provider.server.is_empty() { info!(ctx, "Offline autoconfig found, but no servers defined."); diff --git a/src/constants.rs b/src/constants.rs index a3c29223f8..b1275fd214 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -118,7 +118,7 @@ pub enum Chattype { /// Group chat. /// - /// Created by [`crate::chat::create_group_chat`]. + /// Created by [`crate::chat::create_group`]. Group = 120, /// An (unencrypted) mailing list, diff --git a/src/contact.rs b/src/contact.rs index ad556bc1f5..91b4969b5b 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -36,7 +36,7 @@ use crate::message::MessageState; use crate::mimeparser::AvatarAction; use crate::param::{Param, Params}; use crate::sync::{self, Sync::*}; -use crate::tools::{SystemTime, duration_to_str, get_abs_path, time}; +use crate::tools::{SystemTime, duration_to_str, get_abs_path, time, to_lowercase}; use crate::{chat, chatlist_events, ensure_and_debug_assert_ne, stock_str}; /// Time during which a contact is considered as seen recently. @@ -1574,19 +1574,10 @@ impl Contact { Ok(None) } - /// Get a color for the contact. - /// The color is calculated from the contact's fingerprint (for key-contacts) - /// or email address (for address-contacts) and can be used - /// for an fallback avatar with white initials - /// as well as for headlines in bubbles of group chats. + /// Returns a color for the contact. + /// See [`self::get_color`]. pub fn get_color(&self) -> u32 { - if let Some(fingerprint) = self.fingerprint() { - str_to_color(&fingerprint.hex()) - } else if self.id == ContactId::SELF { - 0x808080 - } else { - str_to_color(&self.addr.to_lowercase()) - } + get_color(self.id == ContactId::SELF, &self.addr, &self.fingerprint()) } /// Gets the contact's status. @@ -1682,6 +1673,21 @@ impl Contact { } } +/// Returns a color for a contact having given attributes. +/// +/// The color is calculated from contact's fingerprint (for key-contacts) or email address (for +/// address-contacts; should be lowercased to avoid allocation) and can be used for an fallback +/// avatar with white initials as well as for headlines in bubbles of group chats. +pub fn get_color(is_self: bool, addr: &str, fingerprint: &Option) -> u32 { + if let Some(fingerprint) = fingerprint { + str_to_color(&fingerprint.hex()) + } else if is_self { + 0x808080 + } else { + str_to_color(&to_lowercase(addr)) + } +} + // Updates the names of the chats which use the contact name. // // This is one of the few duplicated data, however, getting the chat list is easier this way. @@ -1784,9 +1790,7 @@ WHERE type=? AND id IN ( // also unblock mailinglist // if the contact is a mailinglist address explicitly created to allow unblocking if !new_blocking && contact.origin == Origin::MailinglistAddress { - if let Some((chat_id, _, _)) = - chat::get_chat_id_by_grpid(context, &contact.addr).await? - { + if let Some((chat_id, ..)) = chat::get_chat_id_by_grpid(context, &contact.addr).await? { chat_id.unblock_ex(context, Nosync).await?; } } diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs index 90e8b151b8..b2f78630c9 100644 --- a/src/contact/contact_tests.rs +++ b/src/contact/contact_tests.rs @@ -1,7 +1,7 @@ use deltachat_contact_tools::{addr_cmp, may_be_valid_addr}; use super::*; -use crate::chat::{Chat, ProtectionStatus, get_chat_contacts, send_text_msg}; +use crate::chat::{Chat, get_chat_contacts, send_text_msg}; use crate::chatlist::Chatlist; use crate::receive_imf::receive_imf; use crate::securejoin::get_securejoin_qr; @@ -1320,9 +1320,6 @@ async fn test_self_is_verified() -> Result<()> { assert!(contact.get_verifier_id(&alice).await?.is_none()); 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); - Ok(()) } diff --git a/src/context.rs b/src/context.rs index 57c14e01fc..8d160ae9f0 100644 --- a/src/context.rs +++ b/src/context.rs @@ -14,7 +14,7 @@ use pgp::types::PublicKeyTrait; use ratelimit::Ratelimit; use tokio::sync::{Mutex, Notify, RwLock}; -use crate::chat::{ChatId, ProtectionStatus, get_chat_cnt}; +use crate::chat::{ChatId, get_chat_cnt}; use crate::chatlist_events; use crate::config::Config; use crate::constants::{ @@ -30,6 +30,7 @@ use crate::log::{info, warn}; use crate::logged_debug_assert; use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam}; use crate::message::{self, Message, MessageState, MsgId}; +use crate::net::tls::TlsSessionStore; use crate::param::{Param, Params}; use crate::peer_channels::Iroh; use crate::push::PushSubscriber; @@ -297,6 +298,9 @@ pub struct InnerContext { /// True if account has subscribed to push notifications via IMAP. pub(crate) push_subscribed: AtomicBool, + /// TLS session resumption cache. + pub(crate) tls_session_store: TlsSessionStore, + /// Iroh for realtime peer channels. pub(crate) iroh: Arc>>, @@ -475,6 +479,7 @@ impl Context { debug_logging: std::sync::RwLock::new(None), push_subscriber, push_subscribed: AtomicBool::new(false), + tls_session_store: TlsSessionStore::new(), iroh: Arc::new(RwLock::new(None)), self_fingerprint: OnceLock::new(), connectivities: parking_lot::Mutex::new(Vec::new()), @@ -1030,12 +1035,6 @@ impl Context { .await? .to_string(), ); - res.insert( - "protect_autocrypt", - self.get_config_int(Config::ProtectAutocrypt) - .await? - .to_string(), - ); res.insert( "debug_logging", self.get_config_int(Config::DebugLogging).await?.to_string(), @@ -1084,7 +1083,6 @@ impl Context { async fn get_self_report(&self) -> Result { #[derive(Default)] struct ChatNumbers { - protected: u32, opportunistic_dc: u32, opportunistic_mua: u32, unencrypted_dc: u32, @@ -1119,7 +1117,6 @@ impl Context { res += &format!("key_created {key_created}\n"); // how many of the chats active in the last months are: - // - protected // - opportunistic-encrypted and the contact uses Delta Chat // - opportunistic-encrypted and the contact uses a classical MUA // - unencrypted and the contact uses Delta Chat @@ -1128,7 +1125,7 @@ impl Context { let chats = self .sql .query_map( - "SELECT c.protected, m.param, m.msgrmsg + "SELECT m.param, m.msgrmsg FROM chats c JOIN msgs m ON c.id=m.chat_id @@ -1146,23 +1143,20 @@ impl Context { GROUP BY c.id", (DownloadState::Done, ContactId::INFO, three_months_ago), |row| { - let protected: ProtectionStatus = row.get(0)?; let message_param: Params = row.get::<_, String>(1)?.parse().unwrap_or_default(); let is_dc_message: bool = row.get(2)?; - Ok((protected, message_param, is_dc_message)) + Ok((message_param, is_dc_message)) }, |rows| { let mut chats = ChatNumbers::default(); for row in rows { - let (protected, message_param, is_dc_message) = row?; + let (message_param, is_dc_message) = row?; let encrypted = message_param .get_bool(Param::GuaranteeE2ee) .unwrap_or(false); - if protected == ProtectionStatus::Protected { - chats.protected += 1; - } else if encrypted { + if encrypted { if is_dc_message { chats.opportunistic_dc += 1; } else { @@ -1178,7 +1172,6 @@ impl Context { }, ) .await?; - res += &format!("chats_protected {}\n", chats.protected); res += &format!("chats_opportunistic_dc {}\n", chats.opportunistic_dc); res += &format!("chats_opportunistic_mua {}\n", chats.opportunistic_mua); res += &format!("chats_unencrypted_dc {}\n", chats.unencrypted_dc); @@ -1211,9 +1204,6 @@ impl Context { mark_contact_id_as_verified(self, contact_id, Some(ContactId::SELF)).await?; let chat_id = ChatId::create_for_contact(self, contact_id).await?; - chat_id - .set_protection(self, ProtectionStatus::Protected, time(), Some(contact_id)) - .await?; let mut msg = Message::new_text(self.get_self_report().await?); diff --git a/src/context/context_tests.rs b/src/context/context_tests.rs index e80c17448f..03dcaf3bbe 100644 --- a/src/context/context_tests.rs +++ b/src/context/context_tests.rs @@ -604,15 +604,12 @@ async fn test_draft_self_report() -> Result<()> { let chat_id = alice.draft_self_report().await?; let msg = get_chat_msg(&alice, chat_id, 0, 1).await; - assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled); - - let chat = Chat::load_from_db(&alice, chat_id).await?; - assert!(chat.is_protected()); + assert_eq!(msg.get_info_type(), SystemMessage::ChatE2ee); let mut draft = chat_id.get_draft(&alice).await?.unwrap(); assert!(draft.text.starts_with("core_version")); - // Test that sending into the protected chat works: + // Test that sending into the chat works: let _sent = alice.send_msg(chat_id, &mut draft).await; Ok(()) diff --git a/src/ephemeral.rs b/src/ephemeral.rs index b31445f326..84a5805174 100644 --- a/src/ephemeral.rs +++ b/src/ephemeral.rs @@ -376,6 +376,10 @@ pub(crate) async fn start_chat_ephemeral_timers(context: &Context, chat_id: Chat /// `delete_device_after` setting or `ephemeral_timestamp` column. /// /// For each message a row ID, chat id, viewtype and location ID is returned. +/// +/// Unknown viewtypes are returned as `Viewtype::Unknown` +/// and not as errors bubbled up, easily resulting in infinite loop or leaving messages undeleted. +/// (Happens when viewtypes are removed or added on another device which was backup/add-second-device source) async fn select_expired_messages( context: &Context, now: i64, @@ -395,7 +399,11 @@ WHERE |row| { let id: MsgId = row.get("id")?; let chat_id: ChatId = row.get("chat_id")?; - let viewtype: Viewtype = row.get("type")?; + let viewtype: Viewtype = row + .get("type") + .context("Using default viewtype for ephemeral handling.") + .log_err(context) + .unwrap_or_default(); let location_id: u32 = row.get("location_id")?; Ok((id, chat_id, viewtype, location_id)) }, @@ -437,7 +445,11 @@ WHERE |row| { let id: MsgId = row.get("id")?; let chat_id: ChatId = row.get("chat_id")?; - let viewtype: Viewtype = row.get("type")?; + let viewtype: Viewtype = row + .get("type") + .context("Using default viewtype for delete-old handling.") + .log_err(context) + .unwrap_or_default(); let location_id: u32 = row.get("location_id")?; Ok((id, chat_id, viewtype, location_id)) }, diff --git a/src/ephemeral/ephemeral_tests.rs b/src/ephemeral/ephemeral_tests.rs index 56d18de454..988f19ba6e 100644 --- a/src/ephemeral/ephemeral_tests.rs +++ b/src/ephemeral/ephemeral_tests.rs @@ -12,7 +12,7 @@ use crate::receive_imf::receive_imf; use crate::test_utils::{TestContext, TestContextManager}; use crate::timesmearing::MAX_SECONDS_TO_LEND_FROM_FUTURE; use crate::{ - chat::{self, Chat, ChatItem, ProtectionStatus, create_group_chat, send_text_msg}, + chat::{self, Chat, ChatItem, create_group, send_text_msg}, tools::IsNoneOrEmpty, }; @@ -164,7 +164,7 @@ async fn test_ephemeral_enable_disable() -> Result<()> { async fn test_ephemeral_unpromoted() -> Result<()> { let alice = TestContext::new_alice().await; - let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group name").await?; + let chat_id = create_group(&alice, "Group name").await?; // Group is unpromoted, the timer can be changed without sending a message. assert!(chat_id.is_unpromoted(&alice).await?); @@ -799,8 +799,7 @@ async fn test_ephemeral_timer_non_member() -> Result<()> { let bob = &tcm.bob().await; let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await; - let alice_chat_id = - create_group_chat(alice, ProtectionStatus::Unprotected, "Group name").await?; + let alice_chat_id = create_group(alice, "Group name").await?; add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?; send_text_msg(alice, alice_chat_id, "Hi!".to_string()).await?; @@ -826,3 +825,68 @@ async fn test_ephemeral_timer_non_member() -> Result<()> { Ok(()) } + +/// Tests that expiration of a disappearing message +/// with unknown viewtype does not make `delete_expired_messages` fail. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_disappearing_unknown_viewtype() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + let chat = alice.create_chat(bob).await; + + let duration = 60; + chat.id + .set_ephemeral_timer(alice, Timer::Enabled { duration }) + .await?; + + let mut msg = Message::new_text("Expiring message".to_string()); + let _alice_sent_message = alice.send_msg(chat.id, &mut msg).await; + + // Set message viewtype to unassigned + // type 70 that was previously used for videochat invitations. + alice + .sql + .execute("UPDATE msgs SET type=70 WHERE id=?", (msg.id,)) + .await?; + + SystemTime::shift(Duration::from_secs(100)); + + // This should not fail. + delete_expired_messages(alice, time()).await?; + + Ok(()) +} + +/// Tests that deletion of a message with unknown viewtype +/// triggered by `delete_device_after` +/// does not make `delete_expired_messages` fail. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_delete_device_after_unknown_viewtype() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + let chat = alice.create_chat(bob).await; + alice + .set_config(Config::DeleteDeviceAfter, Some("600")) + .await?; + + let mut msg = Message::new_text("Some message".to_string()); + let _alice_sent_message = alice.send_msg(chat.id, &mut msg).await; + + // Set message viewtype to unassigned + // type 70 that was previously used for videochat invitations. + alice + .sql + .execute("UPDATE msgs SET type=70 WHERE id=?", (msg.id,)) + .await?; + + SystemTime::shift(Duration::from_secs(1000)); + + // This should not fail. + delete_expired_messages(alice, time()).await?; + + Ok(()) +} diff --git a/src/events/chatlist_events.rs b/src/events/chatlist_events.rs index 87af729429..93ea7b6803 100644 --- a/src/events/chatlist_events.rs +++ b/src/events/chatlist_events.rs @@ -66,8 +66,7 @@ mod test_chatlist_events { use crate::{ EventType, chat::{ - self, ChatId, ChatVisibility, MuteDuration, ProtectionStatus, create_broadcast, - create_group_chat, set_muted, + self, ChatId, ChatVisibility, MuteDuration, create_broadcast, create_group, set_muted, }, config::Config, constants::*, @@ -138,12 +137,7 @@ mod test_chatlist_events { async fn test_change_chat_visibility() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = tcm.alice().await; - let chat_id = create_group_chat( - &alice, - crate::chat::ProtectionStatus::Unprotected, - "my_group", - ) - .await?; + let chat_id = create_group(&alice, "my_group").await?; chat_id .set_visibility(&alice, ChatVisibility::Pinned) @@ -289,7 +283,7 @@ mod test_chatlist_events { async fn test_delete_chat() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = tcm.alice().await; - let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?; + let chat = create_group(&alice, "My Group").await?; alice.evtracker.clear_events(); chat.delete(&alice).await?; @@ -299,11 +293,11 @@ mod test_chatlist_events { /// Create group chat #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_create_group_chat() -> Result<()> { + async fn test_create_group() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = tcm.alice().await; alice.evtracker.clear_events(); - let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?; + let chat = create_group(&alice, "My Group").await?; wait_for_chatlist_and_specific_item(&alice, chat).await; Ok(()) } @@ -324,7 +318,7 @@ mod test_chatlist_events { async fn test_mute_chat() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = tcm.alice().await; - let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?; + let chat = create_group(&alice, "My Group").await?; alice.evtracker.clear_events(); chat::set_muted(&alice, chat, MuteDuration::Forever).await?; @@ -343,7 +337,7 @@ mod test_chatlist_events { async fn test_mute_chat_expired() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = tcm.alice().await; - let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?; + let chat = create_group(&alice, "My Group").await?; let mute_duration = MuteDuration::Until( std::time::SystemTime::now() @@ -363,7 +357,7 @@ mod test_chatlist_events { async fn test_change_chat_name() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = tcm.alice().await; - let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?; + let chat = create_group(&alice, "My Group").await?; alice.evtracker.clear_events(); chat::set_chat_name(&alice, chat, "New Name").await?; @@ -377,7 +371,7 @@ mod test_chatlist_events { async fn test_change_chat_profile_image() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = tcm.alice().await; - let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?; + let chat = create_group(&alice, "My Group").await?; alice.evtracker.clear_events(); let file = alice.dir.path().join("avatar.png"); @@ -395,9 +389,7 @@ mod test_chatlist_events { let mut tcm = TestContextManager::new(); let alice = tcm.alice().await; let bob = tcm.bob().await; - let chat = alice - .create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[&bob]) - .await; + let chat = alice.create_group_with_members("My Group", &[&bob]).await; let sent_msg = alice.send_text(chat, "Hello").await; let chat_id_for_bob = bob.recv_msg(&sent_msg).await.chat_id; @@ -419,9 +411,7 @@ mod test_chatlist_events { let mut tcm = TestContextManager::new(); let alice = tcm.alice().await; let bob = tcm.bob().await; - let chat = alice - .create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[&bob]) - .await; + let chat = alice.create_group_with_members("My Group", &[&bob]).await; let sent_msg = alice.send_text(chat, "Hello").await; let chat_id_for_bob = bob.recv_msg(&sent_msg).await.chat_id; @@ -438,9 +428,7 @@ mod test_chatlist_events { let mut tcm = TestContextManager::new(); let alice = tcm.alice().await; let bob = tcm.bob().await; - let chat = alice - .create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[&bob]) - .await; + let chat = alice.create_group_with_members("My Group", &[&bob]).await; let sent_msg = alice.send_text(chat, "Hello").await; let chat_id_for_bob = bob.recv_msg(&sent_msg).await.chat_id; @@ -456,7 +444,7 @@ mod test_chatlist_events { async fn test_delete_message() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = tcm.alice().await; - let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?; + let chat = create_group(&alice, "My Group").await?; let message = chat::send_text_msg(&alice, chat, "Hello World".to_owned()).await?; alice.evtracker.clear_events(); @@ -473,9 +461,7 @@ mod test_chatlist_events { let alice = tcm.alice().await; let bob = tcm.bob().await; - let chat = alice - .create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[&bob]) - .await; + let chat = alice.create_group_with_members("My Group", &[&bob]).await; let sent_msg = alice.send_text(chat, "Hello").await; let chat_id_for_bob = bob.recv_msg(&sent_msg).await.chat_id; chat_id_for_bob.accept(&bob).await?; @@ -516,7 +502,7 @@ mod test_chatlist_events { async fn test_update_after_ephemeral_messages() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = tcm.alice().await; - let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?; + let chat = create_group(&alice, "My Group").await?; chat.set_ephemeral_timer(&alice, crate::ephemeral::Timer::Enabled { duration: 60 }) .await?; alice @@ -560,8 +546,7 @@ First thread."#; let alice = tcm.alice().await; let bob = tcm.bob().await; - let alice_chatid = - chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat").await?; + let alice_chatid = chat::create_group(&alice.ctx, "the chat").await?; // Step 1: Generate QR-code, secure-join implied by chatid let qr = get_securejoin_qr(&alice.ctx, Some(alice_chatid)).await?; @@ -608,7 +593,7 @@ First thread."#; async fn test_resend_message() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = tcm.alice().await; - let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?; + let chat = create_group(&alice, "My Group").await?; let msg_id = chat::send_text_msg(&alice, chat, "Hello".to_owned()).await?; let _ = alice.pop_sent_msg().await; @@ -628,7 +613,7 @@ First thread."#; async fn test_reaction() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = tcm.alice().await; - let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?; + let chat = create_group(&alice, "My Group").await?; let msg_id = chat::send_text_msg(&alice, chat, "Hello".to_owned()).await?; let _ = alice.pop_sent_msg().await; diff --git a/src/imap/client.rs b/src/imap/client.rs index 84d123da9b..bd5b13659d 100644 --- a/src/imap/client.rs +++ b/src/imap/client.rs @@ -37,12 +37,12 @@ impl DerefMut for Client { } /// Converts port number to ALPN list. -fn alpn(port: u16) -> &'static [&'static str] { +fn alpn(port: u16) -> &'static str { if port == 993 { // Do not request ALPN on standard port. - &[] + "" } else { - &["imap"] + "imap" } } @@ -210,7 +210,15 @@ impl Client { let account_id = context.get_id(); let events = context.events.clone(); let logging_stream = LoggingStream::new(tcp_stream, account_id, events)?; - let tls_stream = wrap_tls(strict_tls, hostname, alpn(addr.port()), logging_stream).await?; + let tls_stream = wrap_tls( + strict_tls, + hostname, + addr.port(), + alpn(addr.port()), + logging_stream, + &context.tls_session_store, + ) + .await?; let buffered_stream = BufWriter::new(tls_stream); let session_stream: Box = Box::new(buffered_stream); let mut client = Client::new(session_stream); @@ -262,9 +270,16 @@ impl Client { let buffered_tcp_stream = client.into_inner(); let tcp_stream = buffered_tcp_stream.into_inner(); - let tls_stream = wrap_tls(strict_tls, host, &[], tcp_stream) - .await - .context("STARTTLS upgrade failed")?; + let tls_stream = wrap_tls( + strict_tls, + host, + addr.port(), + "", + tcp_stream, + &context.tls_session_store, + ) + .await + .context("STARTTLS upgrade failed")?; let buffered_stream = BufWriter::new(tls_stream); let session_stream: Box = Box::new(buffered_stream); let client = Client::new(session_stream); @@ -281,7 +296,15 @@ impl Client { let proxy_stream = proxy_config .connect(context, domain, port, strict_tls) .await?; - let tls_stream = wrap_tls(strict_tls, domain, alpn(port), proxy_stream).await?; + let tls_stream = wrap_tls( + strict_tls, + domain, + port, + alpn(port), + proxy_stream, + &context.tls_session_store, + ) + .await?; let buffered_stream = BufWriter::new(tls_stream); let session_stream: Box = Box::new(buffered_stream); let mut client = Client::new(session_stream); @@ -334,9 +357,16 @@ impl Client { let buffered_proxy_stream = client.into_inner(); let proxy_stream = buffered_proxy_stream.into_inner(); - let tls_stream = wrap_tls(strict_tls, hostname, &[], proxy_stream) - .await - .context("STARTTLS upgrade failed")?; + let tls_stream = wrap_tls( + strict_tls, + hostname, + port, + "", + proxy_stream, + &context.tls_session_store, + ) + .await + .context("STARTTLS upgrade failed")?; let buffered_stream = BufWriter::new(tls_stream); let session_stream: Box = Box::new(buffered_stream); let client = Client::new(session_stream); diff --git a/src/key.rs b/src/key.rs index 06f25b8eb1..cde859d006 100644 --- a/src/key.rs +++ b/src/key.rs @@ -25,7 +25,7 @@ use crate::tools::{self, time_elapsed}; /// This trait is implemented for rPGP's [SignedPublicKey] and /// [SignedSecretKey] types and makes working with them a little /// easier in the deltachat world. -pub(crate) trait DcKey: Serialize + Deserializable + Clone { +pub trait DcKey: Serialize + Deserializable + Clone { /// Create a key from some bytes. fn from_slice(bytes: &[u8]) -> Result { let res = ::from_bytes(Cursor::new(bytes)); @@ -112,7 +112,10 @@ pub(crate) trait DcKey: Serialize + Deserializable + Clone { /// The fingerprint for the key. fn dc_fingerprint(&self) -> Fingerprint; + /// Whether the key is private (or public). fn is_private() -> bool; + + /// Returns the OpenPGP Key ID. fn key_id(&self) -> KeyId; } diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 11727bf355..298e807219 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -964,10 +964,6 @@ impl MimeFactory { hidden_headers.push(header.clone()); } else if is_hidden(&header_name) { hidden_headers.push(header.clone()); - } else if header_name == "autocrypt" - && !context.get_config_bool(Config::ProtectAutocrypt).await? - { - unprotected_headers.push(header.clone()); } else if header_name == "from" { // Unencrypted securejoin messages should _not_ include the display name: if is_encrypted || !is_securejoin_message { @@ -1085,6 +1081,17 @@ impl MimeFactory { .is_none_or(|ts| now >= ts + gossip_period || now < ts) }; + let verifier_id: Option = context + .sql + .query_get_value( + "SELECT verifier FROM contacts WHERE fingerprint=?", + (&fingerprint,), + ) + .await?; + + let is_verified = + verifier_id.is_some_and(|verifier_id| verifier_id != 0); + if !should_do_gossip { continue; } @@ -1095,7 +1102,7 @@ impl MimeFactory { // Autocrypt 1.1.0 specification says that // `prefer-encrypt` attribute SHOULD NOT be included. prefer_encrypt: EncryptPreference::NoPreference, - verified: false, + verified: is_verified, } .to_string(); @@ -1320,20 +1327,6 @@ impl MimeFactory { let command = msg.param.get_cmd(); let mut placeholdertext = None; - let send_verified_headers = match chat.typ { - Chattype::Single => true, - Chattype::Group => true, - // Mailinglists and broadcast channels can actually never be verified: - Chattype::Mailinglist => false, - Chattype::OutBroadcast | Chattype::InBroadcast => false, - }; - if chat.is_protected() && send_verified_headers { - headers.push(( - "Chat-Verified", - mail_builder::headers::raw::Raw::new("1").into(), - )); - } - if chat.typ == Chattype::Group { // Send group ID unless it is an ad hoc group that has no ID. if !chat.grpid.is_empty() { diff --git a/src/mimefactory/mimefactory_tests.rs b/src/mimefactory/mimefactory_tests.rs index a856a243d3..61cf0fec1e 100644 --- a/src/mimefactory/mimefactory_tests.rs +++ b/src/mimefactory/mimefactory_tests.rs @@ -6,8 +6,7 @@ use std::time::Duration; use super::*; use crate::chat::{ - self, ChatId, ProtectionStatus, add_contact_to_chat, create_group_chat, - remove_contact_from_chat, send_text_msg, + self, ChatId, add_contact_to_chat, create_group, remove_contact_from_chat, send_text_msg, }; use crate::chatlist::Chatlist; use crate::constants; @@ -352,9 +351,7 @@ async fn test_subject_in_group() -> Result<()> { 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 group_id = chat::create_group(&t, "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?; @@ -666,7 +663,7 @@ async fn test_selfavatar_unencrypted_signed() { assert_eq!(part.match_indices("From:").count(), 1); assert_eq!(part.match_indices("Message-ID:").count(), 0); assert_eq!(part.match_indices("Subject:").count(), 1); - assert_eq!(part.match_indices("Autocrypt:").count(), 0); + assert_eq!(part.match_indices("Autocrypt:").count(), 1); assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); let part = payload.next().unwrap(); @@ -717,7 +714,7 @@ async fn test_selfavatar_unencrypted_signed() { assert_eq!(part.match_indices("From:").count(), 1); assert_eq!(part.match_indices("Message-ID:").count(), 0); assert_eq!(part.match_indices("Subject:").count(), 1); - assert_eq!(part.match_indices("Autocrypt:").count(), 0); + assert_eq!(part.match_indices("Autocrypt:").count(), 1); assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); let part = payload.next().unwrap(); @@ -756,7 +753,7 @@ async fn test_remove_member_bcc() -> Result<()> { let charlie_contact = Contact::get_by_id(alice, charlie_id).await?; let charlie_addr = charlie_contact.get_addr(); - let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foo").await?; + let alice_chat_id = create_group(alice, "foo").await?; add_contact_to_chat(alice, alice_chat_id, bob_id).await?; add_contact_to_chat(alice, alice_chat_id, charlie_id).await?; send_text_msg(alice, alice_chat_id, "Creating a group".to_string()).await?; @@ -846,16 +843,12 @@ async fn test_dont_remove_self() -> Result<()> { let alice = &tcm.alice().await; let bob = &tcm.bob().await; - let first_group = alice - .create_group_with_members(ProtectionStatus::Unprotected, "First group", &[bob]) - .await; + let first_group = alice.create_group_with_members("First group", &[bob]).await; alice.send_text(first_group, "Hi! I created a group.").await; remove_contact_from_chat(alice, first_group, ContactId::SELF).await?; alice.pop_sent_msg().await; - let second_group = alice - .create_group_with_members(ProtectionStatus::Unprotected, "First group", &[bob]) - .await; + let second_group = alice.create_group_with_members("First group", &[bob]).await; let sent = alice .send_text(second_group, "Hi! I created another group.") .await; @@ -883,9 +876,7 @@ async fn test_new_member_is_first_recipient() -> Result<()> { let bob_id = alice.add_or_lookup_contact_id(bob).await; let charlie_id = alice.add_or_lookup_contact_id(charlie).await; - let group = alice - .create_group_with_members(ProtectionStatus::Unprotected, "Group", &[bob]) - .await; + let group = alice.create_group_with_members("Group", &[bob]).await; alice.send_text(group, "Hi! I created a group.").await; SystemTime::shift(Duration::from_secs(60)); diff --git a/src/mimeparser.rs b/src/mimeparser.rs index b84235180c..ff374d3612 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -34,7 +34,7 @@ use crate::sync::SyncItems; use crate::tools::{ get_filemeta, parse_receive_headers, smeared_time, time, truncate_msg_text, validate_id, }; -use crate::{chatlist_events, location, stock_str, tools}; +use crate::{chatlist_events, location, tools}; /// Public key extracted from `Autocrypt-Gossip` /// header with associated information. @@ -87,12 +87,12 @@ pub(crate) struct MimeMessage { pub chat_disposition_notification_to: Option, pub decrypting_failed: bool, - /// Set of valid signature fingerprints if a message is an + /// Valid signature fingerprint if a message is an /// Autocrypt encrypted and signed message. /// /// If a message is not encrypted or the signature is not valid, - /// this set is empty. - pub signatures: HashSet, + /// this is `None`. + pub signature: Option, /// The addresses for which there was a gossip header /// and their respective gossiped keys. @@ -589,7 +589,7 @@ impl MimeMessage { decrypting_failed: mail.is_err(), // only non-empty if it was a valid autocrypt message - signatures, + signature: signatures.into_iter().last(), autocrypt_fingerprint, gossiped_keys, is_forwarded: false, @@ -622,13 +622,12 @@ impl MimeMessage { parser.parse_mime_recursive(context, mail, false).await?; } Err(err) => { - let msg_body = stock_str::cant_decrypt_msg_body(context).await; - let txt = format!("[{msg_body}]"); + let txt = "[This message cannot be decrypted.\n\nβ€’ It might already help to simply reply to this message and ask the sender to send the message again.\n\nβ€’ If you just re-installed Delta Chat then it is best if you re-setup Delta Chat now and choose \"Add as second device\" or import a backup.]"; let part = Part { typ: Viewtype::Text, - msg_raw: Some(txt.clone()), - msg: txt, + msg_raw: Some(txt.to_string()), + msg: txt.to_string(), // Don't change the error prefix for now, // receive_imf.rs:lookup_chat_by_reply() checks it. error: Some(format!("Decrypting failed: {err:#}")), @@ -966,7 +965,7 @@ impl MimeMessage { /// This means the message was both encrypted and signed with a /// valid signature. pub fn was_encrypted(&self) -> bool { - !self.signatures.is_empty() + self.signature.is_some() } /// Returns whether the email contains a `chat-version` header. diff --git a/src/mimeparser/mimeparser_tests.rs b/src/mimeparser/mimeparser_tests.rs index 09800f53ec..19060f37c4 100644 --- a/src/mimeparser/mimeparser_tests.rs +++ b/src/mimeparser/mimeparser_tests.rs @@ -1817,39 +1817,6 @@ async fn test_take_last_header() { ); } -async fn test_protect_autocrypt(enabled: bool) -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = &tcm.alice().await; - let bob = &tcm.bob().await; - - let chat = alice.create_chat(bob).await; - alice - .set_config_bool(Config::ProtectAutocrypt, enabled) - .await?; - let sent = alice.send_text(chat.id, "Hello!").await; - assert_eq!(sent.payload().contains("Autocrypt: "), !enabled); - let msg = bob.recv_msg(&sent).await; - assert_eq!(msg.get_showpadlock(), true); - - Ok(()) -} - -/// Tests that if `protect_autocrypt` is enabled, -/// `Autocrypt` header does not appear in the outer headers -/// of encrypted messages. -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_protect_autocrypt_enabled() -> Result<()> { - test_protect_autocrypt(true).await -} - -/// Tests that if `protect_autocrypt` is disabled, -/// `Autocrypt` header appears in the outer headers -/// of encrypted messages. -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_protect_autocrypt_false() -> Result<()> { - test_protect_autocrypt(false).await -} - /// Tests that CRLF before MIME boundary /// is not treated as the part body. /// diff --git a/src/net.rs b/src/net.rs index 01bd8ed9b2..4471a492de 100644 --- a/src/net.rs +++ b/src/net.rs @@ -12,6 +12,7 @@ use tokio_io_timeout::TimeoutStream; use crate::context::Context; use crate::net::session::SessionStream; +use crate::net::tls::TlsSessionStore; use crate::sql::Sql; use crate::tools::time; @@ -127,10 +128,19 @@ pub(crate) async fn connect_tls_inner( addr: SocketAddr, host: &str, strict_tls: bool, - alpn: &[&str], + alpn: &str, + tls_session_store: &TlsSessionStore, ) -> Result { let tcp_stream = connect_tcp_inner(addr).await?; - let tls_stream = wrap_tls(strict_tls, host, alpn, tcp_stream).await?; + let tls_stream = wrap_tls( + strict_tls, + host, + addr.port(), + alpn, + tcp_stream, + tls_session_store, + ) + .await?; Ok(tls_stream) } diff --git a/src/net/http.rs b/src/net/http.rs index 4d09b68ded..68dff5fc7b 100644 --- a/src/net/http.rs +++ b/src/net/http.rs @@ -16,6 +16,10 @@ use crate::net::session::SessionStream; use crate::net::tls::wrap_rustls; use crate::tools::time; +/// User-Agent for HTTP requests if a resource usage policy requires it. +/// By default we do not set User-Agent. +const USER_AGENT: &str = "chatmail/2 (+https://github.com/chatmail/core/)"; + /// HTTP(S) GET response. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Response { @@ -76,11 +80,13 @@ where let proxy_stream = proxy_config .connect(context, host, port, load_cache) .await?; - let tls_stream = wrap_rustls(host, &[], proxy_stream).await?; + let tls_stream = + wrap_rustls(host, port, "", proxy_stream, &context.tls_session_store).await?; Box::new(tls_stream) } else { let tcp_stream = crate::net::connect_tcp(context, host, port, load_cache).await?; - let tls_stream = wrap_rustls(host, &[], tcp_stream).await?; + let tls_stream = + wrap_rustls(host, port, "", tcp_stream, &context.tls_session_store).await?; Box::new(tls_stream) } } @@ -102,6 +108,13 @@ fn http_url_cache_timestamps(url: &str, mimetype: Option<&str>) -> (i64, i64) { let stale = if url.ends_with(".xdc") { // WebXDCs are never stale, they just expire. expires + } else if url.starts_with("https://tile.openstreetmap.org/") + || url.starts_with("https://vector.openstreetmap.org/") + { + // Policy at + // requires that we cache tiles for at least 7 days. + // Do not revalidate earlier than that. + now + 3600 * 24 * 7 } else if mimetype.is_some_and(|s| s.starts_with("image/")) { // Cache images for 1 day. // @@ -243,8 +256,22 @@ async fn fetch_url(context: &Context, original_url: &str) -> Result { .context("URL has no authority")? .clone(); - let req = hyper::Request::builder() - .uri(parsed_url) + let req = hyper::Request::builder().uri(parsed_url); + + // OSM usage policy requires + // that User-Agent is set for HTTP GET requests + // to tile servers: + // + // Same for vectory tiles + // at . + let req = + if authority == "tile.openstreetmap.org" || authority == "vector.openstreetmap.org" { + req.header("User-Agent", USER_AGENT) + } else { + req + }; + + let req = req .header(hyper::header::HOST, authority.as_str()) .body(http_body_util::Empty::::new())?; let response = sender.send_request(req).await?; diff --git a/src/net/proxy.rs b/src/net/proxy.rs index 0f657b5439..c59692ccc8 100644 --- a/src/net/proxy.rs +++ b/src/net/proxy.rs @@ -429,7 +429,14 @@ impl ProxyConfig { load_cache, ) .await?; - let tls_stream = wrap_rustls(&https_config.host, &[], tcp_stream).await?; + let tls_stream = wrap_rustls( + &https_config.host, + https_config.port, + "", + tcp_stream, + &context.tls_session_store, + ) + .await?; let auth = if let Some((username, password)) = &https_config.user_password { Some((username.as_str(), password.as_str())) } else { diff --git a/src/net/tls.rs b/src/net/tls.rs index 1c9eb5a90a..fce4abcb92 100644 --- a/src/net/tls.rs +++ b/src/net/tls.rs @@ -1,27 +1,38 @@ //! TLS support. +use parking_lot::Mutex; +use std::collections::HashMap; use std::sync::Arc; use anyhow::Result; use crate::net::session::SessionStream; +use tokio_rustls::rustls::client::ClientSessionStore; + pub async fn wrap_tls<'a>( strict_tls: bool, hostname: &str, - alpn: &[&str], + port: u16, + alpn: &str, stream: impl SessionStream + 'static, + tls_session_store: &TlsSessionStore, ) -> Result { if strict_tls { - let tls_stream = wrap_rustls(hostname, alpn, stream).await?; + let tls_stream = wrap_rustls(hostname, port, alpn, stream, tls_session_store).await?; let boxed_stream: Box = Box::new(tls_stream); Ok(boxed_stream) } else { // We use native_tls because it accepts 1024-bit RSA keys. // Rustls does not support them even if // certificate checks are disabled: . + let alpns = if alpn.is_empty() { + Box::from([]) + } else { + Box::from([alpn]) + }; let tls = async_native_tls::TlsConnector::new() .min_protocol_version(Some(async_native_tls::Protocol::Tlsv12)) - .request_alpns(alpn) + .request_alpns(&alpns) .danger_accept_invalid_hostnames(true) .danger_accept_invalid_certs(true); let tls_stream = tls.connect(hostname, stream).await?; @@ -30,18 +41,82 @@ pub async fn wrap_tls<'a>( } } +/// Map to store TLS session tickets. +/// +/// Tickets are separated by port and ALPN +/// to avoid trying to use Postfix ticket for Dovecot and vice versa. +/// Doing so would not be a security issue, +/// but wastes the ticket and the opportunity to resume TLS session unnecessarily. +/// Rustls takes care of separating tickets that belong to different domain names. +#[derive(Debug)] +pub(crate) struct TlsSessionStore { + sessions: Mutex>>, +} + +// This is the default for TLS session store +// as of Rustls version 0.23.16, +// but we want to create multiple caches +// to separate them by port and ALPN. +const TLS_CACHE_SIZE: usize = 256; + +impl TlsSessionStore { + /// Creates a new TLS session store. + /// + /// One such store should be created per profile + /// to keep TLS sessions independent. + pub fn new() -> Self { + Self { + sessions: Default::default(), + } + } + + /// Returns session store for given port and ALPN. + /// + /// Rustls additionally separates sessions by hostname. + pub fn get(&self, port: u16, alpn: &str) -> Arc { + Arc::clone( + self.sessions + .lock() + .entry((port, alpn.to_string())) + .or_insert_with(|| { + Arc::new(tokio_rustls::rustls::client::ClientSessionMemoryCache::new( + TLS_CACHE_SIZE, + )) + }), + ) + } +} + pub async fn wrap_rustls<'a>( hostname: &str, - alpn: &[&str], + port: u16, + alpn: &str, stream: impl SessionStream + 'a, + tls_session_store: &TlsSessionStore, ) -> Result { - let mut root_cert_store = rustls::RootCertStore::empty(); + let mut root_cert_store = tokio_rustls::rustls::RootCertStore::empty(); root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); - let mut config = rustls::ClientConfig::builder() + let mut config = tokio_rustls::rustls::ClientConfig::builder() .with_root_certificates(root_cert_store) .with_no_client_auth(); - config.alpn_protocols = alpn.iter().map(|s| s.as_bytes().to_vec()).collect(); + config.alpn_protocols = if alpn.is_empty() { + vec![] + } else { + vec![alpn.as_bytes().to_vec()] + }; + + // Enable TLS 1.3 session resumption + // as defined in . + // + // Obsolete TLS 1.2 mechanisms defined in RFC 5246 + // and RFC 5077 have worse security + // and are not worth increasing + // attack surface: . + let resumption_store = tls_session_store.get(port, alpn); + let resumption = tokio_rustls::rustls::client::Resumption::store(resumption_store) + .tls12_resumption(tokio_rustls::rustls::client::Tls12Resumption::Disabled); + config.resumption = resumption; let tls = tokio_rustls::TlsConnector::from(Arc::new(config)); let name = rustls_pki_types::ServerName::try_from(hostname)?.to_owned(); diff --git a/src/oauth2.rs b/src/oauth2.rs index ae875d8273..5c0b6a00e1 100644 --- a/src/oauth2.rs +++ b/src/oauth2.rs @@ -14,17 +14,6 @@ use crate::provider; use crate::provider::Oauth2Authorizer; use crate::tools::time; -const OAUTH2_GMAIL: Oauth2 = Oauth2 { - // see - client_id: "959970109878-4mvtgf6feshskf7695nfln6002mom908.apps.googleusercontent.com", - get_code: "https://accounts.google.com/o/oauth2/auth?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&response_type=code&scope=https%3A%2F%2Fmail.google.com%2F%20email&access_type=offline", - init_token: "https://accounts.google.com/o/oauth2/token?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&code=$CODE&grant_type=authorization_code", - refresh_token: "https://accounts.google.com/o/oauth2/token?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&refresh_token=$REFRESH_TOKEN&grant_type=refresh_token", - get_userinfo: Some( - "https://www.googleapis.com/oauth2/v1/userinfo?alt=json&access_token=$ACCESS_TOKEN", - ), -}; - const OAUTH2_YANDEX: Oauth2 = Oauth2 { // see client_id: "c4d0b6735fc8420a816d7e1303469341", @@ -64,7 +53,7 @@ pub async fn get_oauth2_url( addr: &str, redirect_uri: &str, ) -> Result> { - if let Some(oauth2) = Oauth2::from_address(context, addr).await { + if let Some(oauth2) = Oauth2::from_address(addr) { context .sql .set_raw_config("oauth2_pending_redirect_uri", Some(redirect_uri)) @@ -84,7 +73,7 @@ pub(crate) async fn get_oauth2_access_token( code: &str, regenerate: bool, ) -> Result> { - if let Some(oauth2) = Oauth2::from_address(context, addr).await { + if let Some(oauth2) = Oauth2::from_address(addr) { let lock = context.oauth2_mutex.lock().await; // read generated token @@ -232,7 +221,7 @@ pub(crate) async fn get_oauth2_addr( addr: &str, code: &str, ) -> Result> { - let oauth2 = match Oauth2::from_address(context, addr).await { + let oauth2 = match Oauth2::from_address(addr) { Some(o) => o, None => return Ok(None), }; @@ -267,19 +256,16 @@ pub(crate) async fn get_oauth2_addr( } impl Oauth2 { - async fn from_address(context: &Context, addr: &str) -> Option { + fn from_address(addr: &str) -> Option { let addr_normalized = normalize_addr(addr); - let skip_mx = true; if let Some(domain) = addr_normalized .find('@') .map(|index| addr_normalized.split_at(index + 1).1) { - if let Some(oauth2_authorizer) = provider::get_provider_info(context, domain, skip_mx) - .await + if let Some(oauth2_authorizer) = provider::get_provider_info(domain) .and_then(|provider| provider.oauth2_authorizer.as_ref()) { return Some(match oauth2_authorizer { - Oauth2Authorizer::Gmail => OAUTH2_GMAIL, Oauth2Authorizer::Yandex => OAUTH2_YANDEX, }); } @@ -366,21 +352,16 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_oauth_from_address() { - let t = TestContext::new().await; - // Delta Chat does not have working Gmail client ID anymore. - assert_eq!(Oauth2::from_address(&t, "hello@gmail.com").await, None); - assert_eq!(Oauth2::from_address(&t, "hello@googlemail.com").await, None); + assert_eq!(Oauth2::from_address("hello@gmail.com"), None); + assert_eq!(Oauth2::from_address("hello@googlemail.com"), None); assert_eq!( - Oauth2::from_address(&t, "hello@yandex.com").await, - Some(OAUTH2_YANDEX) - ); - assert_eq!( - Oauth2::from_address(&t, "hello@yandex.ru").await, + Oauth2::from_address("hello@yandex.com"), Some(OAUTH2_YANDEX) ); - assert_eq!(Oauth2::from_address(&t, "hello@web.de").await, None); + assert_eq!(Oauth2::from_address("hello@yandex.ru"), Some(OAUTH2_YANDEX)); + assert_eq!(Oauth2::from_address("hello@web.de"), None); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] diff --git a/src/peer_channels.rs b/src/peer_channels.rs index c74a4d695a..a8797f8861 100644 --- a/src/peer_channels.rs +++ b/src/peer_channels.rs @@ -574,7 +574,7 @@ mod tests { use super::*; use crate::{ EventType, - chat::{self, ChatId, ProtectionStatus, add_contact_to_chat, resend_msgs, send_msg}, + chat::{self, ChatId, add_contact_to_chat, resend_msgs, send_msg}, message::{Message, Viewtype}, test_utils::{TestContext, TestContextManager}, }; @@ -616,7 +616,7 @@ mod tests { loop { let event = bob.evtracker.recv().await.unwrap(); if let EventType::WebxdcRealtimeAdvertisementReceived { msg_id } = event.typ { - assert!(msg_id == alice_webxdc.id); + assert!(msg_id == bob_webxdc.id); break; } } @@ -962,9 +962,7 @@ mod tests { let mut tcm = TestContextManager::new(); let alice = &mut tcm.alice().await; let bob = &mut tcm.bob().await; - let group = chat::create_group_chat(alice, ProtectionStatus::Unprotected, "group chat") - .await - .unwrap(); + let group = chat::create_group(alice, "group chat").await.unwrap(); // Alice sends webxdc to bob let mut instance = Message::new(Viewtype::File); diff --git a/src/pgp.rs b/src/pgp.rs index 81380b0bb6..86d39aeb1d 100644 --- a/src/pgp.rs +++ b/src/pgp.rs @@ -178,7 +178,7 @@ pub async fn pk_encrypt( let msg = MessageBuilder::from_bytes("", plain); let mut msg = msg.seipd_v1(&mut rng, SYMMETRIC_KEY_ALGORITHM); for pkey in pkeys { - msg.encrypt_to_key(&mut rng, &pkey)?; + msg.encrypt_to_key_anonymous(&mut rng, &pkey)?; } if let Some(ref skey) = private_key_for_signing { @@ -347,6 +347,8 @@ mod tests { use super::*; use crate::test_utils::{alice_keypair, bob_keypair}; + use pgp::composed::Esk; + use pgp::packet::PublicKeyEncryptedSessionKey; fn pk_decrypt_and_validate<'a>( ctext: &'a [u8], @@ -543,4 +545,34 @@ mod tests { assert_eq!(content, CLEARTEXT); assert_eq!(valid_signatures.len(), 0); } + + /// Tests that recipient key IDs and fingerprints + /// are omitted or replaced with wildcards. + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_anonymous_recipients() -> Result<()> { + let ctext = ctext_signed().await.as_bytes(); + let cursor = Cursor::new(ctext); + let (msg, _headers) = Message::from_armor(cursor)?; + + let Message::Encrypted { esk, .. } = msg else { + unreachable!(); + }; + + for encrypted_session_key in esk { + let Esk::PublicKeyEncryptedSessionKey(pkesk) = encrypted_session_key else { + unreachable!() + }; + + match pkesk { + PublicKeyEncryptedSessionKey::V3 { id, .. } => { + assert!(id.is_wildcard()); + } + PublicKeyEncryptedSessionKey::V6 { fingerprint, .. } => { + assert!(fingerprint.is_none()); + } + PublicKeyEncryptedSessionKey::Other { .. } => unreachable!(), + } + } + Ok(()) + } } diff --git a/src/provider.rs b/src/provider.rs index 24de61ada3..9d5ee63c00 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -4,13 +4,9 @@ pub(crate) mod data; use anyhow::Result; use deltachat_contact_tools::EmailAddress; -use hickory_resolver::name_server::TokioConnectionProvider; -use hickory_resolver::{Resolver, TokioResolver, config}; use serde::{Deserialize, Serialize}; use crate::config::Config; -use crate::context::Context; -use crate::log::warn; use crate::provider::data::{PROVIDER_DATA, PROVIDER_IDS}; /// Provider status according to manual testing. @@ -82,13 +78,9 @@ pub enum UsernamePattern { /// Type of OAuth 2 authorization. #[derive(Debug, PartialEq, Eq)] -#[repr(u8)] pub enum Oauth2Authorizer { /// Yandex. - Yandex = 1, - - /// Gmail. - Gmail = 2, + Yandex, } /// Email server endpoint. @@ -175,61 +167,17 @@ impl ProviderOptions { } } -/// Get resolver to query MX records. -/// -/// We first try to read the system's resolver from `/etc/resolv.conf`. -/// This does not work at least on some Androids, therefore we fallback -/// to the default `ResolverConfig` which uses eg. to google's `8.8.8.8` or `8.8.4.4`. -fn get_resolver() -> Result { - if let Ok(resolver) = TokioResolver::builder_tokio() { - return Ok(resolver.build()); - } - let resolver = Resolver::builder_with_config( - config::ResolverConfig::default(), - TokioConnectionProvider::default(), - ); - Ok(resolver.build()) -} - /// Returns provider for the given an e-mail address. /// /// Returns an error if provided address is not valid. -pub async fn get_provider_info_by_addr( - context: &Context, - addr: &str, - skip_mx: bool, -) -> Result> { +pub fn get_provider_info_by_addr(addr: &str) -> Result> { let addr = EmailAddress::new(addr)?; - let provider = get_provider_info(context, &addr.domain, skip_mx).await; - Ok(provider) -} - -/// Returns provider for the given domain. -/// -/// This function looks up domain in offline database first. If not -/// found, it queries MX record for the domain and looks up offline -/// database for MX domains. -pub async fn get_provider_info( - context: &Context, - domain: &str, - skip_mx: bool, -) -> Option<&'static Provider> { - if let Some(provider) = get_provider_by_domain(domain) { - return Some(provider); - } - - if !skip_mx { - if let Some(provider) = get_provider_by_mx(context, domain).await { - return Some(provider); - } - } - - None + Ok(get_provider_info(&addr.domain)) } /// Finds a provider in offline database based on domain. -pub fn get_provider_by_domain(domain: &str) -> Option<&'static Provider> { +pub fn get_provider_info(domain: &str) -> Option<&'static Provider> { let domain = domain.to_lowercase(); for (pattern, provider) in PROVIDER_DATA { if let Some(suffix) = pattern.strip_prefix('*') { @@ -247,51 +195,6 @@ pub fn get_provider_by_domain(domain: &str) -> Option<&'static Provider> { None } -/// Finds a provider based on MX record for the given domain. -/// -/// For security reasons, only Gmail can be configured this way. -pub async fn get_provider_by_mx(context: &Context, domain: &str) -> Option<&'static Provider> { - let Ok(resolver) = get_resolver() else { - warn!(context, "Cannot get a resolver to check MX records."); - return None; - }; - - let mut fqdn: String = domain.to_string(); - if !fqdn.ends_with('.') { - fqdn.push('.'); - } - - let Ok(mx_domains) = resolver.mx_lookup(fqdn).await else { - warn!(context, "Cannot resolve MX records for {domain:?}."); - return None; - }; - - for (provider_domain_pattern, provider) in PROVIDER_DATA { - if provider.id != "gmail" { - // MX lookup is limited to Gmail for security reasons - continue; - } - - if provider_domain_pattern.starts_with('*') { - // Skip wildcard patterns. - continue; - } - - let provider_fqdn = provider_domain_pattern.to_string() + "."; - let provider_fqdn_dot = ".".to_string() + &provider_fqdn; - - for mx_domain in mx_domains.iter() { - let mx_domain = mx_domain.exchange().to_lowercase().to_utf8(); - - if mx_domain == provider_fqdn || mx_domain.ends_with(&provider_fqdn_dot) { - return Some(provider); - } - } - } - - None -} - /// Returns a provider with the given ID from the database. pub fn get_provider_by_id(id: &str) -> Option<&'static Provider> { if let Some(provider) = PROVIDER_IDS.get(id) { @@ -304,24 +207,23 @@ pub fn get_provider_by_id(id: &str) -> Option<&'static Provider> { #[cfg(test)] mod tests { use super::*; - use crate::test_utils::TestContext; #[test] fn test_get_provider_by_domain_unexistant() { - let provider = get_provider_by_domain("unexistant.org"); + let provider = get_provider_info("unexistant.org"); assert!(provider.is_none()); } #[test] fn test_get_provider_by_domain_mixed_case() { - let provider = get_provider_by_domain("nAUta.Cu").unwrap(); + let provider = get_provider_info("nAUta.Cu").unwrap(); assert!(provider.status == Status::Ok); } #[test] - fn test_get_provider_by_domain() { + fn test_get_provider_info() { let addr = "nauta.cu"; - let provider = get_provider_by_domain(addr).unwrap(); + let provider = get_provider_info(addr).unwrap(); assert!(provider.status == Status::Ok); let server = &provider.server[0]; assert_eq!(server.protocol, Protocol::Imap); @@ -336,13 +238,17 @@ mod tests { assert_eq!(server.port, 25); assert_eq!(server.username_pattern, UsernamePattern::Email); - let provider = get_provider_by_domain("gmail.com").unwrap(); + let provider = get_provider_info("gmail.com").unwrap(); assert!(provider.status == Status::Preparation); assert!(!provider.before_login_hint.is_empty()); assert!(!provider.overview_page.is_empty()); - let provider = get_provider_by_domain("googlemail.com").unwrap(); + let provider = get_provider_info("googlemail.com").unwrap(); assert!(provider.status == Status::Preparation); + + assert!(get_provider_info("").is_none()); + assert!(get_provider_info("google.com").unwrap().id == "gmail"); + assert!(get_provider_info("example@google.com").is_none()); } #[test] @@ -351,39 +257,10 @@ mod tests { assert!(provider.id == "gmail"); } - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_provider_info() { - let t = TestContext::new().await; - assert!(get_provider_info(&t, "", false).await.is_none()); - assert!(get_provider_info(&t, "google.com", false).await.unwrap().id == "gmail"); - assert!( - get_provider_info(&t, "example@google.com", false) - .await - .is_none() - ); - } - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_get_provider_info_by_addr() -> Result<()> { - let t = TestContext::new().await; - assert!( - get_provider_info_by_addr(&t, "google.com", false) - .await - .is_err() - ); - assert!( - get_provider_info_by_addr(&t, "example@google.com", false) - .await? - .unwrap() - .id - == "gmail" - ); - Ok(()) - } - - #[test] - fn test_get_resolver() -> Result<()> { - assert!(get_resolver().is_ok()); + assert!(get_provider_info_by_addr("google.com").is_err()); + assert!(get_provider_info_by_addr("example@google.com")?.unwrap().id == "gmail"); Ok(()) } } diff --git a/src/qr.rs b/src/qr.rs index ede3d33f88..aa2cfc19f1 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -19,7 +19,7 @@ use crate::key::Fingerprint; use crate::net::http::post_empty; use crate::net::proxy::{DEFAULT_SOCKS_PORT, ProxyConfig}; use crate::token; -use crate::tools::validate_id; +use crate::tools::{time, validate_id}; const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase const IDELTACHAT_SCHEME: &str = "https://i.delta.chat/#"; @@ -741,8 +741,16 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> { authcode, .. } => { - token::save(context, token::Namespace::InviteNumber, None, &invitenumber).await?; - token::save(context, token::Namespace::Auth, None, &authcode).await?; + let timestamp = time(); + token::save( + context, + token::Namespace::InviteNumber, + None, + &invitenumber, + timestamp, + ) + .await?; + token::save(context, token::Namespace::Auth, None, &authcode, timestamp).await?; context.sync_qr_code_tokens(None).await?; context.scheduler.interrupt_inbox().await; } @@ -752,14 +760,23 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> { grpid, .. } => { + let timestamp = time(); token::save( context, token::Namespace::InviteNumber, Some(&grpid), &invitenumber, + timestamp, + ) + .await?; + token::save( + context, + token::Namespace::Auth, + Some(&grpid), + &authcode, + timestamp, ) .await?; - token::save(context, token::Namespace::Auth, Some(&grpid), &authcode).await?; context.sync_qr_code_tokens(Some(&grpid)).await?; context.scheduler.interrupt_inbox().await; } diff --git a/src/qr/qr_tests.rs b/src/qr/qr_tests.rs index e0358d31e4..41d55d4fea 100644 --- a/src/qr/qr_tests.rs +++ b/src/qr/qr_tests.rs @@ -1,5 +1,5 @@ use super::*; -use crate::chat::{ProtectionStatus, create_group_chat}; +use crate::chat::create_group; use crate::config::Config; use crate::securejoin::get_securejoin_qr; use crate::test_utils::{TestContext, TestContextManager, sync}; @@ -479,7 +479,7 @@ async fn test_withdraw_verifycontact() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_withdraw_verifygroup() -> Result<()> { let alice = TestContext::new_alice().await; - let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?; + let chat_id = create_group(&alice, "foo").await?; let qr = get_securejoin_qr(&alice, Some(chat_id)).await?; // scanning own verify-group code offers withdrawing @@ -520,8 +520,8 @@ async fn test_withdraw_multidevice() -> Result<()> { // Alice creates two QR codes on the first device: // group QR code and contact QR code. - let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?; - let chat2_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group 2").await?; + let chat_id = create_group(alice, "Group").await?; + let chat2_id = create_group(alice, "Group 2").await?; let contact_qr = get_securejoin_qr(alice, None).await?; let group_qr = get_securejoin_qr(alice, Some(chat_id)).await?; let group2_qr = get_securejoin_qr(alice, Some(chat2_id)).await?; diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 47903206c1..e64a74a7e4 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -14,9 +14,7 @@ use mailparse::SingleInfo; use num_traits::FromPrimitive; use regex::Regex; -use crate::chat::{ - self, Chat, ChatId, ChatIdBlocked, ProtectionStatus, remove_from_chat_contacts_table, -}; +use crate::chat::{self, Chat, ChatId, ChatIdBlocked, remove_from_chat_contacts_table}; use crate::config::Config; use crate::constants::{self, Blocked, Chattype, DC_CHAT_ID_TRASH, EDITED_PREFIX, ShowEmails}; use crate::contact::{Contact, ContactId, Origin, mark_contact_id_as_verified}; @@ -248,9 +246,7 @@ async fn get_to_and_past_contact_ids( let chat_id = match chat_assignment { ChatAssignment::Trash => None, ChatAssignment::GroupChat { grpid } => { - if let Some((chat_id, _protected, _blocked)) = - chat::get_chat_id_by_grpid(context, grpid).await? - { + if let Some((chat_id, _blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? { Some(chat_id) } else { None @@ -642,7 +638,7 @@ 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 fingerprint = mime_parser.signature.as_ref(); let (from_id, _from_id_blocked, incoming_origin) = match from_field_to_contact_id( context, &mime_parser.from, @@ -742,7 +738,7 @@ pub(crate) async fn receive_imf_inner( 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?; + mark_recipients_as_verified(context, from_id, &mime_parser).await?; } let received_msg = if let Some(received_msg) = received_msg { @@ -794,7 +790,6 @@ pub(crate) async fn receive_imf_inner( allow_creation, &mut mime_parser, is_partial_download, - &verified_encryption, parent_message, ) .await?; @@ -812,7 +807,6 @@ pub(crate) async fn receive_imf_inner( is_partial_download, replace_msg_id, prevent_rename, - verified_encryption, chat_id, chat_id_blocked, is_dc_message, @@ -860,7 +854,9 @@ pub(crate) async fn receive_imf_inner( if let Some(ref sync_items) = mime_parser.sync_items { if from_id == ContactId::SELF { if mime_parser.was_encrypted() { - context.execute_sync_items(sync_items).await; + context + .execute_sync_items(sync_items, mime_parser.timestamp_sent) + .await; } else { warn!(context, "Sync items are not encrypted."); } @@ -1170,7 +1166,8 @@ async fn decide_chat_assignment( .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); + let txt = "⚠️ It seems you are using Delta Chat on multiple devices that cannot decrypt each other's outgoing messages. To fix this, on the older device use \"Settings / Add Second Device\" and follow the instructions."; + let mut msg = Message::new_text(txt.to_string()); chat::add_device_msg(context, None, Some(&mut msg)) .await .log_err(context) @@ -1319,7 +1316,6 @@ async fn do_chat_assignment( allow_creation: bool, mime_parser: &mut MimeMessage, is_partial_download: Option, - verified_encryption: &VerifiedEncryption, parent_message: Option, ) -> Result<(ChatId, Blocked)> { let is_bot = context.get_config_bool(Config::Bot).await?; @@ -1362,9 +1358,7 @@ async fn do_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? - { + if let Some((id, 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() { @@ -1376,7 +1370,6 @@ async fn do_chat_assignment( from_id, to_ids, past_ids, - verified_encryption, grpid, ) .await? @@ -1478,45 +1471,6 @@ async fn do_chat_assignment( ); } } - - // Check if the message was sent with verified encryption and set the protection of - // the 1:1 chat accordingly. - let chat = match is_partial_download.is_none() - && mime_parser.get_header(HeaderDef::SecureJoin).is_none() - { - true => Some(Chat::load_from_db(context, chat_id).await?) - .filter(|chat| chat.typ == Chattype::Single), - false => None, - }; - if let Some(chat) = chat { - ensure_and_debug_assert!( - chat.typ == Chattype::Single, - "Chat {chat_id} is not Single", - ); - let new_protection = match verified_encryption { - VerifiedEncryption::Verified => ProtectionStatus::Protected, - VerifiedEncryption::NotVerified(_) => ProtectionStatus::Unprotected, - }; - - ensure_and_debug_assert!( - chat.protected == ProtectionStatus::Unprotected - || new_protection == ProtectionStatus::Protected, - "Chat {chat_id} can't downgrade to Unprotected", - ); - if chat.protected != new_protection { - // The message itself will be sorted under the device message since the device - // message is `MessageState::InNoticed`, which means that all following - // messages are sorted under it. - chat_id - .set_protection( - context, - new_protection, - mime_parser.timestamp_sent, - Some(from_id), - ) - .await?; - } - } } } } else { @@ -1533,9 +1487,7 @@ async fn do_chat_assignment( 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? - { + if let Some((id, blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? { chat_id = Some(id); chat_id_blocked = blocked; } else if allow_creation { @@ -1547,7 +1499,6 @@ async fn do_chat_assignment( from_id, to_ids, past_ids, - verified_encryption, grpid, ) .await? @@ -1603,7 +1554,7 @@ async fn do_chat_assignment( if chat_id.is_none() && allow_creation { let to_contact = Contact::get_by_id(context, to_id).await?; if let Some(list_id) = to_contact.param.get(Param::ListId) { - if let Some((id, _, blocked)) = + if let Some((id, blocked)) = chat::get_chat_id_by_grpid(context, list_id).await? { chat_id = Some(id); @@ -1669,7 +1620,6 @@ async fn add_parts( is_partial_download: Option, mut replace_msg_id: Option, prevent_rename: bool, - verified_encryption: VerifiedEncryption, chat_id: ChatId, chat_id_blocked: Blocked, is_dc_message: MessengerMessage, @@ -1718,16 +1668,7 @@ async fn add_parts( apply_out_broadcast_changes(context, mime_parser, &mut chat, from_id).await? } Chattype::Group => { - apply_group_changes( - context, - mime_parser, - &mut chat, - from_id, - to_ids, - past_ids, - &verified_encryption, - ) - .await? + apply_group_changes(context, mime_parser, &mut chat, from_id, to_ids, past_ids).await? } Chattype::InBroadcast => { apply_in_broadcast_changes(context, mime_parser, &mut chat, from_id).await? @@ -2224,6 +2165,7 @@ RETURNING id if !chat_id.is_trash() && !hidden { let mut chat = Chat::load_from_db(context, chat_id).await?; + let mut update_param = false; // In contrast to most other update-timestamps, // use `sort_timestamp` instead of `sent_timestamp` for the subject-timestamp comparison. @@ -2237,6 +2179,14 @@ RETURNING id let subject = mime_parser.get_subject().unwrap_or_default(); chat.param.set(Param::LastSubject, subject); + update_param = true; + } + + if chat.is_unpromoted() { + chat.param.remove(Param::Unpromoted); + update_param = true; + } + if update_param { chat.update_param(context).await?; } } @@ -2632,27 +2582,12 @@ async fn create_group( from_id: ContactId, to_ids: &[Option], past_ids: &[Option], - verified_encryption: &VerifiedEncryption, grpid: &str, ) -> Result> { 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(); - let create_protected = if mime_parser.get_header(HeaderDef::ChatVerified).is_some() { - if let VerifiedEncryption::NotVerified(err) = verified_encryption { - warn!( - context, - "Creating unprotected group because of the verification problem: {err:#}." - ); - ProtectionStatus::Unprotected - } else { - ProtectionStatus::Protected - } - } else { - ProtectionStatus::Unprotected - }; - async fn self_explicitly_added( context: &Context, mime_parser: &&mut MimeMessage, @@ -2688,7 +2623,6 @@ async fn create_group( grpid, grpname, create_blocked, - create_protected, None, mime_parser.timestamp_sent, ) @@ -2860,7 +2794,6 @@ async fn apply_group_changes( from_id: ContactId, to_ids: &[Option], past_ids: &[Option], - verified_encryption: &VerifiedEncryption, ) -> Result { let to_ids_flat: Vec = to_ids.iter().filter_map(|x| *x).collect(); ensure!(chat.typ == Chattype::Group); @@ -2875,24 +2808,6 @@ async fn apply_group_changes( let is_from_in_chat = !chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id); - if mime_parser.get_header(HeaderDef::ChatVerified).is_some() && !chat.is_protected() { - if let VerifiedEncryption::NotVerified(err) = verified_encryption { - warn!( - context, - "Not marking chat {} as protected due to verification problem: {err:#}.", chat.id, - ); - } else { - chat.id - .set_protection( - context, - ProtectionStatus::Protected, - mime_parser.timestamp_sent, - Some(from_id), - ) - .await?; - } - } - 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, @@ -3280,7 +3195,7 @@ async fn create_or_lookup_mailinglist_or_broadcast( ) -> Result> { let listid = mailinglist_header_listid(list_id_header)?; - if let Some((chat_id, _, blocked)) = chat::get_chat_id_by_grpid(context, &listid).await? { + if let Some((chat_id, blocked)) = chat::get_chat_id_by_grpid(context, &listid).await? { return Ok(Some((chat_id, blocked))); } @@ -3318,7 +3233,6 @@ async fn create_or_lookup_mailinglist_or_broadcast( &listid, name, blocked, - ProtectionStatus::Unprotected, param, mime_parser.timestamp_sent, ) @@ -3599,7 +3513,6 @@ async fn create_adhoc_group( "", // Ad hoc groups have no ID. grpname, create_blocked, - ProtectionStatus::Unprotected, None, mime_parser.timestamp_sent, ) @@ -3662,7 +3575,10 @@ async fn has_verified_encryption( )); } - let signed_with_verified_key = mimeparser.signatures.contains(&fingerprint); + let signed_with_verified_key = mimeparser + .signature + .as_ref() + .is_some_and(|signature| *signature == fingerprint); if signed_with_verified_key { Ok(Verified) } else { @@ -3675,7 +3591,6 @@ async fn has_verified_encryption( async fn mark_recipients_as_verified( context: &Context, from_id: ContactId, - to_ids: &[Option], mimeparser: &MimeMessage, ) -> Result<()> { let verifier_id = Some(from_id).filter(|&id| id != ContactId::SELF); @@ -3694,19 +3609,6 @@ async fn mark_recipients_as_verified( } mark_contact_id_as_verified(context, to_id, verifier_id).await?; - ChatId::set_protection_for_contact(context, to_id, mimeparser.timestamp_sent).await?; - } - - if mimeparser.get_header(HeaderDef::ChatVerified).is_none() { - return Ok(()); - } - for to_id in to_ids.iter().filter_map(|&x| x) { - if to_id == ContactId::SELF || to_id == from_id { - continue; - } - - mark_contact_id_as_verified(context, to_id, verifier_id).await?; - ChatId::set_protection_for_contact(context, to_id, mimeparser.timestamp_sent).await?; } Ok(()) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 6c2e56204a..b347a73a48 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -5,7 +5,7 @@ use tokio::fs; use super::*; use crate::chat::{ - ChatItem, ChatVisibility, add_contact_to_chat, add_to_chat_contacts_table, create_group_chat, + ChatItem, ChatVisibility, add_contact_to_chat, add_to_chat_contacts_table, create_group, get_chat_contacts, get_chat_msgs, is_contact_in_chat, remove_contact_from_chat, send_text_msg, }; use crate::chatlist::Chatlist; @@ -1000,7 +1000,7 @@ async fn test_other_device_writes_to_mailinglist() -> Result<()> { chat::get_chat_id_by_grpid(&t, "delta.codespeak.net") .await? .unwrap(), - (first_chat.id, false, Blocked::Request) + (first_chat.id, Blocked::Request) ); receive_imf( @@ -2901,9 +2901,9 @@ async fn test_accept_outgoing() -> Result<()> { let bob1_chat = bob1.create_chat(&alice1).await; let sent = bob1.send_text(bob1_chat.id, "Hello!").await; - alice1.recv_msg(&sent).await; + let alice1_msg = alice1.recv_msg(&sent).await; alice2.recv_msg(&sent).await; - let alice1_msg = bob2.recv_msg(&sent).await; + bob2.recv_msg(&sent).await; assert_eq!(alice1_msg.text, "Hello!"); let alice1_chat = chat::Chat::load_from_db(&alice1, alice1_msg.chat_id).await?; assert!(alice1_chat.is_contact_request()); @@ -2944,7 +2944,7 @@ async fn test_outgoing_private_reply_multidevice() -> Result<()> { let charlie = tcm.charlie().await; // =============== Bob creates a group =============== - let group_id = chat::create_group_chat(&bob, ProtectionStatus::Unprotected, "Group").await?; + let group_id = chat::create_group(&bob, "Group").await?; chat::add_to_chat_contacts_table( &bob, time(), @@ -3042,9 +3042,7 @@ async fn test_auto_accept_protected_group_for_bots() -> Result<()> { bob.set_config(Config::Bot, Some("1")).await.unwrap(); mark_as_verified(alice, bob).await; mark_as_verified(bob, alice).await; - let group_id = alice - .create_group_with_members(ProtectionStatus::Protected, "Group", &[bob]) - .await; + let group_id = alice.create_group_with_members("Group", &[bob]).await; let sent = alice.send_text(group_id, "Hello!").await; let msg = bob.recv_msg(&sent).await; let chat = chat::Chat::load_from_db(bob, msg.chat_id).await?; @@ -3092,13 +3090,11 @@ async fn test_bot_accepts_another_group_after_qr_scan() -> Result<()> { let bob = &tcm.bob().await; bob.set_config(Config::Bot, Some("1")).await?; - let group_id = chat::create_group_chat(alice, ProtectionStatus::Protected, "Group").await?; + let group_id = chat::create_group(alice, "Group").await?; let qr = get_securejoin_qr(alice, Some(group_id)).await?; tcm.exec_securejoin_qr(bob, alice, &qr).await; - let group_id = alice - .create_group_with_members(ProtectionStatus::Protected, "Group", &[bob]) - .await; + let group_id = alice.create_group_with_members("Group", &[bob]).await; let sent = alice.send_text(group_id, "Hello!").await; let msg = bob.recv_msg(&sent).await; let chat = chat::Chat::load_from_db(bob, msg.chat_id).await?; @@ -3152,7 +3148,7 @@ async fn test_no_private_reply_to_blocked_account() -> Result<()> { let bob = tcm.bob().await; tcm.section("Bob creates a group"); - let group_id = chat::create_group_chat(&bob, ProtectionStatus::Unprotected, "Group").await?; + let group_id = chat::create_group(&bob, "Group").await?; chat::add_to_chat_contacts_table( &bob, time(), @@ -3228,11 +3224,7 @@ async fn test_blocked_contact_creates_group() -> Result<()> { chat.id.block(&alice).await?; let group_id = bob - .create_group_with_members( - ProtectionStatus::Unprotected, - "group name", - &[&alice, &fiona], - ) + .create_group_with_members("group name", &[&alice, &fiona]) .await; let sent = bob.send_text(group_id, "Heyho, I'm a spammer!").await; @@ -3253,7 +3245,7 @@ async fn test_blocked_contact_creates_group() -> Result<()> { assert_eq!(rcvd.chat_blocked, Blocked::Request); // In order not to lose context, Bob's message should also be shown in the group let msgs = chat::get_chat_msgs(&alice, rcvd.chat_id).await?; - assert_eq!(msgs.len(), 2); + assert_eq!(msgs.len(), 3); Ok(()) } @@ -3279,7 +3271,7 @@ async fn test_outgoing_undecryptable() -> Result<()> { assert!( dev_msg .text - .contains(&stock_str::cant_decrypt_outgoing_msgs(alice).await) + .contains("⚠️ It seems you are using Delta Chat on multiple devices that cannot decrypt each other's outgoing messages. To fix this, on the older device use \"Settings / Add Second Device\" and follow the instructions.") ); let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_signed.eml"); @@ -3728,7 +3720,7 @@ async fn test_unsigned_chat_group_hdr() -> Result<()> { let bob = &tcm.bob().await; let bob_addr = bob.get_config(Config::Addr).await?.unwrap(); let bob_id = alice.add_or_lookup_contact_id(bob).await; - let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foos").await?; + let alice_chat_id = create_group(alice, "foos").await?; add_contact_to_chat(alice, alice_chat_id, bob_id).await?; send_text_msg(alice, alice_chat_id, "populate".to_string()).await?; let sent_msg = alice.pop_sent_msg().await; @@ -3774,7 +3766,7 @@ async fn test_sync_member_list_on_rejoin() -> Result<()> { let bob_id = alice.add_or_lookup_contact_id(bob).await; let fiona_id = alice.add_or_lookup_contact_id(fiona).await; - let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foos").await?; + let alice_chat_id = create_group(alice, "foos").await?; add_contact_to_chat(alice, alice_chat_id, bob_id).await?; add_contact_to_chat(alice, alice_chat_id, fiona_id).await?; @@ -3812,7 +3804,7 @@ async fn test_ignore_outdated_membership_changes() -> Result<()> { let alice = &tcm.alice().await; let bob = &tcm.bob().await; let alice_bob_id = alice.add_or_lookup_contact_id(bob).await; - let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "grp").await?; + let alice_chat_id = create_group(alice, "grp").await?; // Alice creates a group chat. Bob accepts it. add_contact_to_chat(alice, alice_chat_id, alice_bob_id).await?; @@ -3860,7 +3852,7 @@ async fn test_dont_recreate_contacts_on_add_remove() -> Result<()> { let fiona = &tcm.fiona().await; let charlie = &tcm.charlie().await; - let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?; + let alice_chat_id = create_group(alice, "Group").await?; add_contact_to_chat( alice, @@ -3911,7 +3903,7 @@ async fn test_delayed_removal_is_ignored() -> Result<()> { let alice = &tcm.alice().await; let bob = &tcm.bob().await; let fiona = &tcm.fiona().await; - let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?; + let chat_id = create_group(alice, "Group").await?; let alice_bob = alice.add_or_lookup_contact_id(bob).await; let alice_fiona = alice.add_or_lookup_contact_id(fiona).await; // create chat with three members @@ -3964,7 +3956,7 @@ async fn test_dont_readd_with_normal_msg() -> Result<()> { let bob = &tcm.bob().await; let fiona = &tcm.fiona().await; - let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?; + let alice_chat_id = create_group(alice, "Group").await?; add_contact_to_chat( alice, @@ -4202,7 +4194,7 @@ async fn test_member_left_does_not_create_chat() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = &tcm.alice().await; let bob = &tcm.bob().await; - let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?; + let alice_chat_id = create_group(alice, "Group").await?; add_contact_to_chat( alice, alice_chat_id, @@ -4230,7 +4222,7 @@ async fn test_recreate_member_list_on_missing_add_of_self() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = &tcm.alice().await; let bob = &tcm.bob().await; - let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?; + let alice_chat_id = create_group(alice, "Group").await?; add_contact_to_chat( alice, alice_chat_id, @@ -4274,7 +4266,7 @@ async fn test_recreate_member_list_on_missing_add_of_self() -> Result<()> { 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?; + let alice_chat_id = create_group(&alice, "Group").await?; add_contact_to_chat( &alice, alice_chat_id, @@ -4414,7 +4406,7 @@ async fn test_create_group_with_big_msg() -> Result<()> { let file_bytes = include_bytes!("../../test-data/image/screenshot.png"); - let bob_grp_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "Group").await?; + let bob_grp_id = create_group(&bob, "Group").await?; add_contact_to_chat(&bob, bob_grp_id, ba_contact).await?; let mut msg = Message::new(Viewtype::Image); msg.set_file_from_bytes(&bob, "a.jpg", file_bytes, None)?; @@ -4445,7 +4437,7 @@ async fn test_create_group_with_big_msg() -> Result<()> { // Now Bob can send encrypted messages to Alice. - let bob_grp_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "Group1").await?; + let bob_grp_id = create_group(&bob, "Group1").await?; add_contact_to_chat(&bob, bob_grp_id, ba_contact).await?; let mut msg = Message::new(Viewtype::Image); msg.set_file_from_bytes(&bob, "a.jpg", file_bytes, None)?; @@ -4486,7 +4478,7 @@ async fn test_partial_group_consistency() -> Result<()> { let bob = tcm.bob().await; let fiona = tcm.fiona().await; let bob_id = alice.add_or_lookup_contact_id(&bob).await; - let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foos").await?; + let alice_chat_id = create_group(&alice, "foos").await?; add_contact_to_chat(&alice, alice_chat_id, bob_id).await?; send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?; @@ -4566,7 +4558,7 @@ async fn test_protected_group_add_remove_member_missing_key() -> Result<()> { let alice = &tcm.alice().await; let bob = &tcm.bob().await; mark_as_verified(alice, bob).await; - let group_id = create_group_chat(alice, ProtectionStatus::Protected, "Group").await?; + let group_id = create_group(alice, "Group").await?; 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; @@ -4663,7 +4655,7 @@ async fn test_unarchive_on_member_removal() -> Result<()> { let fiona = &tcm.fiona().await; let bob_id = alice.add_or_lookup_contact_id(bob).await; let fiona_id = alice.add_or_lookup_contact_id(fiona).await; - let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foos").await?; + let alice_chat_id = create_group(alice, "foos").await?; add_contact_to_chat(alice, alice_chat_id, bob_id).await?; add_contact_to_chat(alice, alice_chat_id, fiona_id).await?; @@ -4696,9 +4688,7 @@ async fn test_no_op_member_added_is_trash() -> Result<()> { 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; + let alice_chat_id = alice.create_group_with_members("foos", &[bob]).await; send_text_msg(alice, alice_chat_id, "populate".to_string()).await?; let msg = alice.pop_sent_msg().await; bob.recv_msg(&msg).await; @@ -4765,7 +4755,7 @@ async fn test_references() -> Result<()> { let bob = &tcm.bob().await; alice.set_config_bool(Config::BccSelf, true).await?; - let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?; + let alice_chat_id = create_group(alice, "Group").await?; alice .send_text(alice_chat_id, "Hi! I created a group.") .await; @@ -4809,7 +4799,7 @@ async fn test_prefer_references_to_downloaded_msgs() -> Result<()> { let fiona = &tcm.fiona().await; let alice_bob_id = tcm.send_recv(bob, alice, "hi").await.from_id; let alice_fiona_id = tcm.send_recv(fiona, alice, "hi").await.from_id; - let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?; + let alice_chat_id = create_group(alice, "Group").await?; add_contact_to_chat(alice, alice_chat_id, alice_bob_id).await?; // W/o fiona the test doesn't work -- the last message is assigned to the 1:1 chat due to // `is_probably_private_reply()`. @@ -5009,12 +4999,7 @@ 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(alice, "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; @@ -5032,7 +5017,7 @@ async fn test_rename_chat_on_missing_message() -> Result<()> { 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").await?; + let chat_id = create_group(&alice, "Group").await?; add_to_chat_contacts_table( &alice, time(), @@ -5068,7 +5053,7 @@ async fn test_rename_chat_after_creating_invite() -> Result<()> { let alice = &tcm.alice().await; let bob = &tcm.bob().await; for populate_before_securejoin in [false, true] { - let alice_chat_id = create_group_chat(alice, ProtectionStatus::Protected, "Group").await?; + let alice_chat_id = create_group(alice, "Group").await?; let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await?; SystemTime::shift(Duration::from_secs(60)); @@ -5098,7 +5083,7 @@ async fn test_two_group_securejoins() -> Result<()> { let bob = &tcm.bob().await; let fiona = &tcm.fiona().await; - let group_id = chat::create_group_chat(alice, ProtectionStatus::Protected, "Group").await?; + let group_id = chat::create_group(alice, "Group").await?; let qr = get_securejoin_qr(alice, Some(group_id)).await?; @@ -5123,8 +5108,7 @@ async fn test_unverified_member_msg() -> Result<()> { let bob = &tcm.bob().await; let fiona = &tcm.fiona().await; - let alice_chat_id = - chat::create_group_chat(alice, ProtectionStatus::Protected, "Group").await?; + let alice_chat_id = chat::create_group(alice, "Group").await?; let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await?; tcm.exec_securejoin_qr(bob, alice, &qr).await; @@ -5150,12 +5134,15 @@ async fn test_dont_reverify_by_self_on_outgoing_msg() -> Result<()> { let bob = &tcm.bob().await; let fiona = &tcm.fiona().await; - let bob_chat_id = chat::create_group_chat(bob, ProtectionStatus::Protected, "Group").await?; + let bob_chat_id = chat::create_group(bob, "Group").await?; let qr = get_securejoin_qr(bob, Some(bob_chat_id)).await?; tcm.exec_securejoin_qr(fiona, bob, &qr).await; tcm.exec_securejoin_qr(a0, bob, &qr).await; tcm.exec_securejoin_qr(a1, bob, &qr).await; + // Shift time by one week to trigger gossip. + SystemTime::shift(Duration::from_secs(7 * 24 * 3600)); + let a0_chat_id = a0.get_last_msg().await.chat_id; let a0_sent_msg = a0.send_text(a0_chat_id, "Hi").await; a1.recv_msg(&a0_sent_msg).await; @@ -5211,9 +5198,7 @@ async fn test_no_address_contact_added_into_group() -> Result<()> { 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 alice_chat_id = alice.create_group_with_members("Group", &[bob]).await; let bob_received_msg = bob .recv_msg(&alice.send_text(alice_chat_id, "Message").await) .await; @@ -5493,7 +5478,7 @@ async fn test_small_unencrypted_group() -> Result<()> { let alice = &tcm.alice().await; let bob = &tcm.bob().await; - let alice_chat_id = chat::create_group_ex(alice, None, "Unencrypted group").await?; + let alice_chat_id = chat::create_group_unencrypted(alice, "Unencrypted group").await?; let alice_bob_id = alice.add_or_lookup_address_contact_id(bob).await; add_contact_to_chat(alice, alice_chat_id, alice_bob_id).await?; send_text_msg(alice, alice_chat_id, "Hello!".to_string()).await?; diff --git a/src/securejoin.rs b/src/securejoin.rs index 04642ac945..4e3bea5bb8 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -4,8 +4,7 @@ use anyhow::{Context as _, Error, Result, bail, ensure}; use deltachat_contact_tools::ContactAddress; use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode}; -use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus, get_chat_id_by_grpid}; -use crate::chatlist_events; +use crate::chat::{self, Chat, ChatId, ChatIdBlocked, get_chat_id_by_grpid}; use crate::config::Config; use crate::constants::{Blocked, Chattype, NON_ALPHANUMERIC_WITHOUT_DOT}; use crate::contact::mark_contact_id_as_verified; @@ -23,6 +22,7 @@ use crate::qr::check_qr; use crate::securejoin::bob::JoinerProgress; use crate::sync::Sync::*; use crate::token; +use crate::tools::{create_id, time}; mod bob; mod qrinvite; @@ -87,10 +87,21 @@ pub async fn get_securejoin_qr(context: &Context, group: Option) -> Resu let sync_token = token::lookup(context, Namespace::InviteNumber, grpid) .await? .is_none(); - // invitenumber will be used to allow starting the handshake, - // auth will be used to verify the fingerprint + // Invite number is used to request the inviter key. let invitenumber = token::lookup_or_new(context, Namespace::InviteNumber, grpid).await?; - let auth = token::lookup_or_new(context, Namespace::Auth, grpid).await?; + + // Auth token is used to verify the key-contact + // if the token is not old + // and add the contact to the group + // if there is an associated group ID. + // + // We always generate a new auth token + // because auth tokens "expire" + // and can only be used to join groups + // without verification afterwards. + let auth = create_id(); + token::save(context, Namespace::Auth, grpid, &auth, time()).await?; + let self_addr = context.get_primary_self_addr().await?; let self_name = context .get_config(Config::Displayname) @@ -378,7 +389,19 @@ pub(crate) async fn handle_securejoin_handshake( ); return Ok(HandshakeMessage::Ignore); }; - let Some(grpid) = token::auth_foreign_key(context, auth).await? else { + let Some((grpid, timestamp)) = context + .sql + .query_row_optional( + "SELECT foreign_key, timestamp FROM tokens WHERE namespc=? AND token=?", + (Namespace::Auth, auth), + |row| { + let foreign_key: String = row.get(0)?; + let timestamp: i64 = row.get(1)?; + Ok((foreign_key, timestamp)) + }, + ) + .await? + else { warn!( context, "Ignoring {step} message because of invalid auth code." @@ -396,7 +419,11 @@ pub(crate) async fn handle_securejoin_handshake( } }; - if !verify_sender_by_fingerprint(context, &fingerprint, contact_id).await? { + let sender_contact = Contact::get_by_id(context, contact_id).await?; + if sender_contact + .fingerprint() + .is_none_or(|fp| fp != fingerprint) + { warn!( context, "Ignoring {step} message because of fingerprint mismatch." @@ -404,6 +431,11 @@ pub(crate) async fn handle_securejoin_handshake( return Ok(HandshakeMessage::Ignore); } info!(context, "Fingerprint verified via Auth code.",); + + // Mark the contact as verified if auth code is 600 seconds old. + if time() < timestamp + 600 { + mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?; + } 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 @@ -414,13 +446,6 @@ pub(crate) async fn handle_securejoin_handshake( context.emit_event(EventType::ContactsChanged(Some(contact_id))); if let Some(group_chat_id) = group_chat_id { // Join group. - secure_connection_established( - context, - contact_id, - group_chat_id, - mime_message.timestamp_sent, - ) - .await?; chat::add_contact_to_chat_ex(context, Nosync, group_chat_id, contact_id, true) .await?; let is_group = true; @@ -431,13 +456,6 @@ pub(crate) async fn handle_securejoin_handshake( } else { let chat_id = info_chat_id(context, contact_id).await?; // Setup verified contact. - secure_connection_established( - context, - contact_id, - chat_id, - mime_message.timestamp_sent, - ) - .await?; send_alice_handshake_msg(context, contact_id, "vc-contact-confirm") .await .context("failed sending vc-contact-confirm message")?; @@ -560,8 +578,6 @@ pub(crate) async fn observe_securejoin_on_other_device( mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?; - ChatId::set_protection_for_contact(context, contact_id, mime_message.timestamp_sent).await?; - if step == "vg-member-added" || step == "vc-contact-confirm" { let is_group = mime_message .get_header(HeaderDef::ChatGroupMemberAdded) @@ -592,28 +608,6 @@ pub(crate) async fn observe_securejoin_on_other_device( } } -async fn secure_connection_established( - context: &Context, - contact_id: ContactId, - chat_id: ChatId, - timestamp: i64, -) -> Result<()> { - let private_chat_id = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Yes) - .await? - .id; - private_chat_id - .set_protection( - context, - ProtectionStatus::Protected, - timestamp, - Some(contact_id), - ) - .await?; - context.emit_event(EventType::ChatModified(chat_id)); - chatlist_events::emit_chatlist_item_changed(context, chat_id); - Ok(()) -} - /* ****************************************************************************** * Tools: Misc. ******************************************************************************/ @@ -623,17 +617,19 @@ fn encrypted_and_signed( mimeparser: &MimeMessage, expected_fingerprint: &Fingerprint, ) -> bool { - if !mimeparser.was_encrypted() { + if let Some(signature) = mimeparser.signature.as_ref() { + if signature == expected_fingerprint { + true + } else { + warn!( + context, + "Message does not match expected fingerprint {expected_fingerprint}.", + ); + false + } + } else { warn!(context, "Message not encrypted.",); false - } else if !mimeparser.signatures.contains(expected_fingerprint) { - warn!( - context, - "Message does not match expected fingerprint {}.", expected_fingerprint, - ); - false - } else { - true } } diff --git a/src/securejoin/bob.rs b/src/securejoin/bob.rs index 5392f94692..2d7046f5d4 100644 --- a/src/securejoin/bob.rs +++ b/src/securejoin/bob.rs @@ -4,7 +4,7 @@ use anyhow::{Context as _, Result}; use super::HandshakeMessage; use super::qrinvite::QrInvite; -use crate::chat::{self, ChatId, ProtectionStatus, is_contact_in_chat}; +use crate::chat::{self, ChatId, is_contact_in_chat}; use crate::constants::{Blocked, Chattype}; use crate::contact::Origin; use crate::context::Context; @@ -74,16 +74,6 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::RequestWithAuth) .await?; - // Mark 1:1 chat as verified already. - chat_id - .set_protection( - context, - ProtectionStatus::Protected, - time(), - Some(invite.contact_id()), - ) - .await?; - context.emit_event(EventType::SecurejoinJoinerProgress { contact_id: invite.contact_id(), progress: JoinerProgress::RequestWithAuthSent.to_usize(), @@ -123,21 +113,19 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul let ts_sort = chat_id .calc_sort_timestamp(context, 0, sort_to_bottom, received, incoming) .await?; - if chat_id.is_protected(context).await? == ProtectionStatus::Unprotected { - let ts_start = time(); - chat::add_info_msg_with_cmd( - context, - chat_id, - &stock_str::securejoin_wait(context).await, - SystemMessage::SecurejoinWait, - ts_sort, - Some(ts_start), - None, - None, - None, - ) - .await?; - } + let ts_start = time(); + chat::add_info_msg_with_cmd( + context, + chat_id, + &stock_str::securejoin_wait(context).await, + SystemMessage::SecurejoinWait, + ts_sort, + Some(ts_start), + None, + None, + None, + ) + .await?; Ok(chat_id) } } @@ -215,15 +203,6 @@ pub(super) async fn handle_auth_required( } } - chat_id - .set_protection( - context, - ProtectionStatus::Protected, - message.timestamp_sent, - Some(invite.contact_id()), - ) - .await?; - context.emit_event(EventType::SecurejoinJoinerProgress { contact_id: invite.contact_id(), progress: JoinerProgress::RequestWithAuthSent.to_usize(), @@ -348,7 +327,7 @@ async fn joining_chat_id( QrInvite::Contact { .. } => Ok(alice_chat_id), QrInvite::Group { grpid, name, .. } => { let group_chat_id = match chat::get_chat_id_by_grpid(context, grpid).await? { - Some((chat_id, _protected, _blocked)) => { + Some((chat_id, _blocked)) => { chat_id.unblock_ex(context, Nosync).await?; chat_id } @@ -359,7 +338,6 @@ async fn joining_chat_id( grpid, name, Blocked::Not, - ProtectionStatus::Unprotected, // protection is added later as needed None, create_smeared_timestamp(context), ) diff --git a/src/securejoin/securejoin_tests.rs b/src/securejoin/securejoin_tests.rs index ac98cd947b..1c33b115e9 100644 --- a/src/securejoin/securejoin_tests.rs +++ b/src/securejoin/securejoin_tests.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use deltachat_contact_tools::EmailAddress; use super::*; @@ -5,19 +7,17 @@ use crate::chat::{CantSendReason, remove_contact_from_chat}; use crate::chatlist::Chatlist; use crate::constants::Chattype; use crate::key::self_fingerprint; -use crate::mimeparser::GossipedKey; +use crate::mimeparser::{GossipedKey, SystemMessage}; use crate::receive_imf::receive_imf; use crate::stock_str::{self, messages_e2e_encrypted}; use crate::test_utils::{ TestContext, TestContextManager, TimeShiftFalsePositiveNote, get_chat_msg, }; use crate::tools::SystemTime; -use std::time::Duration; #[derive(PartialEq)] enum SetupContactCase { Normal, - CheckProtectionTimestamp, WrongAliceGossip, AliceIsBot, AliceHasName, @@ -28,11 +28,6 @@ async fn test_setup_contact() { test_setup_contact_ex(SetupContactCase::Normal).await } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_setup_contact_protection_timestamp() { - test_setup_contact_ex(SetupContactCase::CheckProtectionTimestamp).await -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_setup_contact_wrong_alice_gossip() { test_setup_contact_ex(SetupContactCase::WrongAliceGossip).await @@ -164,10 +159,6 @@ async fn test_setup_contact_ex(case: SetupContactCase) { assert!(sent.payload.contains("Auto-Submitted: auto-replied")); assert!(!sent.payload.contains("Bob Examplenet")); let mut msg = alice.parse_msg(&sent).await; - let vc_request_with_auth_ts_sent = msg - .get_header(HeaderDef::Date) - .and_then(|value| mailparse::dateparse(value).ok()) - .unwrap(); assert!(msg.was_encrypted()); assert_eq!( msg.get_header(HeaderDef::SecureJoin).unwrap(), @@ -214,10 +205,6 @@ async fn test_setup_contact_ex(case: SetupContactCase) { assert_eq!(contact_bob.is_verified(&alice).await.unwrap(), false); assert_eq!(contact_bob.get_authname(), ""); - if case == SetupContactCase::CheckProtectionTimestamp { - SystemTime::shift(Duration::from_secs(3600)); - } - 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); @@ -247,9 +234,6 @@ async fn test_setup_contact_ex(case: SetupContactCase) { assert!(msg.is_info()); let expected_text = messages_e2e_encrypted(&alice).await; assert_eq!(msg.get_text(), expected_text); - if case == SetupContactCase::CheckProtectionTimestamp { - assert_eq!(msg.timestamp_sort, vc_request_with_auth_ts_sent + 1); - } } // Make sure Alice hasn't yet sent their name to Bob. @@ -292,10 +276,10 @@ async fn test_setup_contact_ex(case: SetupContactCase) { 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); + assert_eq!(msg.get_text(), messages_e2e_encrypted(&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(), messages_e2e_encrypted(&bob).await); + assert_eq!(msg.get_text(), stock_str::securejoin_wait(&bob).await); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -448,8 +432,7 @@ async fn test_secure_join() -> Result<()> { assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0); assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0); - let alice_chatid = - chat::create_group_chat(&alice, ProtectionStatus::Protected, "the chat").await?; + let alice_chatid = chat::create_group(&alice, "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(); @@ -583,7 +566,7 @@ async fn test_secure_join() -> Result<()> { Blocked::Yes, "Alice's 1:1 chat with Bob is not hidden" ); - // There should be 3 messages in the chat: + // There should be 2 messages in the chat: // - The ChatProtectionEnabled message // - You added member bob@example.net let msg = get_chat_msg(&alice, alice_chatid, 0, 2).await; @@ -619,7 +602,6 @@ async fn test_secure_join() -> Result<()> { } let bob_chat = Chat::load_from_db(&bob.ctx, bob_chatid).await?; - assert!(bob_chat.is_protected()); assert!(bob_chat.typ == Chattype::Group); // On this "happy path", Alice and Bob get only a group-chat where all information are added to. @@ -667,7 +649,7 @@ async fn test_unknown_sender() -> Result<()> { tcm.execute_securejoin(&alice, &bob).await; let alice_chat_id = alice - .create_group_with_members(ProtectionStatus::Protected, "Group with Bob", &[&bob]) + .create_group_with_members("Group with Bob", &[&bob]) .await; let sent = alice.send_text(alice_chat_id, "Hi!").await; @@ -733,10 +715,8 @@ async fn test_parallel_securejoin() -> Result<()> { let alice = &tcm.alice().await; let bob = &tcm.bob().await; - let alice_chat1_id = - chat::create_group_chat(alice, ProtectionStatus::Protected, "First chat").await?; - let alice_chat2_id = - chat::create_group_chat(alice, ProtectionStatus::Protected, "Second chat").await?; + let alice_chat1_id = chat::create_group(alice, "First chat").await?; + let alice_chat2_id = chat::create_group(alice, "Second chat").await?; let qr1 = get_securejoin_qr(alice, Some(alice_chat1_id)).await?; let qr2 = get_securejoin_qr(alice, Some(alice_chat2_id)).await?; @@ -862,3 +842,120 @@ async fn test_wrong_auth_token() -> Result<()> { Ok(()) } + +/// Tests that scanning a QR code week later +/// allows Bob to establish a contact with Alice, +/// but does not mark Bob as verified for Alice. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_expired_contact_auth_token() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + // Alice creates a QR code. + let qr = get_securejoin_qr(alice, None).await?; + + // One week passes, QR code expires. + SystemTime::shift(Duration::from_secs(7 * 24 * 3600)); + + // Bob scans the QR code. + join_securejoin(bob, &qr).await?; + + // vc-request + alice.recv_msg_trash(&bob.pop_sent_msg().await).await; + + // vc-auth-requried + bob.recv_msg_trash(&alice.pop_sent_msg().await).await; + + // vc-request-with-auth + alice.recv_msg_trash(&bob.pop_sent_msg().await).await; + + // Bob should not be verified for Alice. + let contact_bob = alice.add_or_lookup_contact_no_key(bob).await; + assert_eq!(contact_bob.is_verified(alice).await.unwrap(), false); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_expired_group_auth_token() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + let alice_chat_id = chat::create_group(alice, "Group").await?; + + // Alice creates a group QR code. + let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await.unwrap(); + + // One week passes, QR code expires. + SystemTime::shift(Duration::from_secs(7 * 24 * 3600)); + + // Bob scans the QR code. + join_securejoin(bob, &qr).await?; + + // vg-request + alice.recv_msg_trash(&bob.pop_sent_msg().await).await; + + // vg-auth-requried + bob.recv_msg_trash(&alice.pop_sent_msg().await).await; + + // vg-request-with-auth + alice.recv_msg_trash(&bob.pop_sent_msg().await).await; + + // vg-member-added + let bob_member_added_msg = bob.recv_msg(&alice.pop_sent_msg().await).await; + assert!(bob_member_added_msg.is_info()); + assert_eq!( + bob_member_added_msg.get_info_type(), + SystemMessage::MemberAddedToGroup + ); + + // Bob should not be verified for Alice. + let contact_bob = alice.add_or_lookup_contact_no_key(bob).await; + assert_eq!(contact_bob.is_verified(alice).await.unwrap(), false); + + Ok(()) +} + +/// Tests that old token is considered expired +/// even if sync message just arrived. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_expired_synced_auth_token() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let alice2 = &tcm.alice().await; + let bob = &tcm.bob().await; + + alice.set_config_bool(Config::SyncMsgs, true).await?; + alice2.set_config_bool(Config::SyncMsgs, true).await?; + + // Alice creates a QR code on the second device. + let qr = get_securejoin_qr(alice2, None).await?; + + alice2.send_sync_msg().await.unwrap(); + let sync_msg = alice2.pop_sent_sync_msg().await; + + // One week passes, QR code expires. + SystemTime::shift(Duration::from_secs(7 * 24 * 3600)); + + alice.recv_msg_trash(&sync_msg).await; + + // Bob scans the QR code. + join_securejoin(bob, &qr).await?; + + // vc-request + alice.recv_msg_trash(&bob.pop_sent_msg().await).await; + + // vc-auth-requried + bob.recv_msg_trash(&alice.pop_sent_msg().await).await; + + // vc-request-with-auth + alice.recv_msg_trash(&bob.pop_sent_msg().await).await; + + // Bob should not be verified for Alice. + let contact_bob = alice.add_or_lookup_contact_no_key(bob).await; + assert_eq!(contact_bob.is_verified(alice).await.unwrap(), false); + + Ok(()) +} diff --git a/src/smtp/connect.rs b/src/smtp/connect.rs index 1910b3de1f..f7b35d6579 100644 --- a/src/smtp/connect.rs +++ b/src/smtp/connect.rs @@ -12,20 +12,20 @@ use crate::login_param::{ConnectionCandidate, ConnectionSecurity}; use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp}; use crate::net::proxy::ProxyConfig; use crate::net::session::SessionBufStream; -use crate::net::tls::wrap_tls; +use crate::net::tls::{TlsSessionStore, wrap_tls}; use crate::net::{ connect_tcp_inner, connect_tls_inner, run_connection_attempts, update_connection_history, }; use crate::oauth2::get_oauth2_access_token; use crate::tools::time; -/// Converts port number to ALPN list. -fn alpn(port: u16) -> &'static [&'static str] { +/// Converts port number to ALPN. +fn alpn(port: u16) -> &'static str { if port == 465 { // Do not request ALPN on standard port. - &[] + "" } else { - &["smtp"] + "smtp" } } @@ -109,8 +109,12 @@ async fn connection_attempt( "Attempting SMTP connection to {host} ({resolved_addr})." ); let res = match security { - ConnectionSecurity::Tls => connect_secure(resolved_addr, host, strict_tls).await, - ConnectionSecurity::Starttls => connect_starttls(resolved_addr, host, strict_tls).await, + ConnectionSecurity::Tls => { + connect_secure(resolved_addr, host, strict_tls, &context.tls_session_store).await + } + ConnectionSecurity::Starttls => { + connect_starttls(resolved_addr, host, strict_tls, &context.tls_session_store).await + } ConnectionSecurity::Plain => connect_insecure(resolved_addr).await, }; match res { @@ -226,7 +230,15 @@ async fn connect_secure_proxy( let proxy_stream = proxy_config .connect(context, hostname, port, strict_tls) .await?; - let tls_stream = wrap_tls(strict_tls, hostname, alpn(port), proxy_stream).await?; + let tls_stream = wrap_tls( + strict_tls, + hostname, + port, + alpn(port), + proxy_stream, + &context.tls_session_store, + ) + .await?; let mut buffered_stream = BufStream::new(tls_stream); skip_smtp_greeting(&mut buffered_stream).await?; let session_stream: Box = Box::new(buffered_stream); @@ -249,9 +261,16 @@ async fn connect_starttls_proxy( skip_smtp_greeting(&mut buffered_stream).await?; let transport = new_smtp_transport(buffered_stream).await?; let tcp_stream = transport.starttls().await?.into_inner(); - let tls_stream = wrap_tls(strict_tls, hostname, &[], tcp_stream) - .await - .context("STARTTLS upgrade failed")?; + let tls_stream = wrap_tls( + strict_tls, + hostname, + port, + "", + tcp_stream, + &context.tls_session_store, + ) + .await + .context("STARTTLS upgrade failed")?; let buffered_stream = BufStream::new(tls_stream); let session_stream: Box = Box::new(buffered_stream); Ok(session_stream) @@ -274,8 +293,16 @@ async fn connect_secure( addr: SocketAddr, hostname: &str, strict_tls: bool, + tls_session_store: &TlsSessionStore, ) -> Result> { - let tls_stream = connect_tls_inner(addr, hostname, strict_tls, alpn(addr.port())).await?; + let tls_stream = connect_tls_inner( + addr, + hostname, + strict_tls, + alpn(addr.port()), + tls_session_store, + ) + .await?; let mut buffered_stream = BufStream::new(tls_stream); skip_smtp_greeting(&mut buffered_stream).await?; let session_stream: Box = Box::new(buffered_stream); @@ -286,6 +313,7 @@ async fn connect_starttls( addr: SocketAddr, host: &str, strict_tls: bool, + tls_session_store: &TlsSessionStore, ) -> Result> { let tcp_stream = connect_tcp_inner(addr).await?; @@ -294,9 +322,16 @@ async fn connect_starttls( skip_smtp_greeting(&mut buffered_stream).await?; let transport = new_smtp_transport(buffered_stream).await?; let tcp_stream = transport.starttls().await?.into_inner(); - let tls_stream = wrap_tls(strict_tls, host, &[], tcp_stream) - .await - .context("STARTTLS upgrade failed")?; + let tls_stream = wrap_tls( + strict_tls, + host, + addr.port(), + "", + tcp_stream, + tls_session_store, + ) + .await + .context("STARTTLS upgrade failed")?; let buffered_stream = BufStream::new(tls_stream); let session_stream: Box = Box::new(buffered_stream); diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 4a7c40e911..54aac6506b 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -19,7 +19,7 @@ use crate::key::DcKey; use crate::log::{info, warn}; use crate::login_param::ConfiguredLoginParam; use crate::message::MsgId; -use crate::provider::get_provider_by_domain; +use crate::provider::get_provider_info; use crate::sql::Sql; use crate::tools::{Time, inc_and_check, time_elapsed}; @@ -382,7 +382,7 @@ UPDATE chats SET protected=1, type=120 WHERE type=130;"#, context .set_config_internal( Config::ConfiguredProvider, - get_provider_by_domain(&domain).map(|provider| provider.id), + get_provider_info(&domain).map(|provider| provider.id), ) .await?; } else { @@ -1261,6 +1261,16 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint); .await?; } + inc_and_check(&mut migration_version, 134)?; + if dbversion < migration_version { + // Reset all indirect verifications. + sql.execute_migration( + "UPDATE contacts SET verifier=0 WHERE verifier!=1", + migration_version, + ) + .await?; + } + let new_version = sql .get_raw_config_int(VERSION_CFG) .await? diff --git a/src/sql/migrations/migrations_tests.rs b/src/sql/migrations/migrations_tests.rs index d3ed58898f..0517f12e51 100644 --- a/src/sql/migrations/migrations_tests.rs +++ b/src/sql/migrations/migrations_tests.rs @@ -122,7 +122,7 @@ async fn test_key_contacts_migration_email2() -> Result<()> { .await? .is_empty() ); - let pgp_bob = Contact::get_by_id(&t, ContactId::new(11)).await?; + let pgp_bob = Contact::get_by_id(&t, ContactId::new(11001)).await?; assert_eq!(pgp_bob.is_key_contact(), true); assert_eq!(pgp_bob.origin, Origin::Hidden); @@ -159,7 +159,10 @@ async fn test_key_contacts_migration_verified() -> Result<()> { INSERT INTO chats_contacts VALUES(10,10,1745609547,0); "#, )?)).await?; - t.sql.run_migrations(&t).await?; + + STOP_MIGRATIONS_AT + .scope(133, t.sql.run_migrations(&t)) + .await?; // Hidden address-contact can't be looked up. assert!( diff --git a/src/stock_str.rs b/src/stock_str.rs index 45548c2647..381e9a81fa 100644 --- a/src/stock_str.rs +++ b/src/stock_str.rs @@ -11,7 +11,7 @@ use tokio::sync::RwLock; use crate::accounts::Accounts; use crate::blob::BlobObject; -use crate::chat::{self, Chat, ChatId, ProtectionStatus}; +use crate::chat::{self, Chat, ChatId}; use crate::config::Config; use crate::contact::{Contact, ContactId, Origin}; use crate::context::Context; @@ -71,9 +71,6 @@ pub enum StockMessage { #[strum(props(fallback = "No encryption"))] EncrNone = 28, - #[strum(props(fallback = "This message was encrypted for another setup."))] - CantDecryptMsgBody = 29, - #[strum(props(fallback = "Fingerprints"))] FingerPrints = 30, @@ -392,11 +389,6 @@ pub enum StockMessage { ))] InvalidUnencryptedMail = 174, - #[strum(props( - fallback = "⚠️ It seems you are using Delta Chat on multiple devices that cannot decrypt each other's outgoing messages. To fix this, on the older device use \"Settings / Add Second Device\" and follow the instructions." - ))] - CantDecryptOutgoingMsgs = 175, - #[strum(props(fallback = "You reacted %1$s to \"%2$s\""))] MsgYouReacted = 176, @@ -763,16 +755,6 @@ pub(crate) async fn encr_none(context: &Context) -> String { translated(context, StockMessage::EncrNone).await } -/// Stock string: `This message was encrypted for another setup.`. -pub(crate) async fn cant_decrypt_msg_body(context: &Context) -> String { - translated(context, StockMessage::CantDecryptMsgBody).await -} - -/// Stock string:`Got outgoing message(s) encrypted for another setup...`. -pub(crate) async fn cant_decrypt_outgoing_msgs(context: &Context) -> String { - translated(context, StockMessage::CantDecryptOutgoingMsgs).await -} - /// Stock string: `Fingerprints`. pub(crate) async fn finger_prints(context: &Context) -> String { translated(context, StockMessage::FingerPrints).await @@ -1083,13 +1065,6 @@ pub(crate) async fn messages_e2e_encrypted(context: &Context) -> String { translated(context, StockMessage::ChatProtectionEnabled).await } -/// Stock string: `%1$s sent a message from another device.` -pub(crate) async fn chat_protection_disabled(context: &Context, contact_id: ContactId) -> String { - translated(context, StockMessage::ChatProtectionDisabled) - .await - .replace1(&contact_id.get_stock_name(context).await) -} - /// Stock string: `Reply`. pub(crate) async fn reply_noun(context: &Context) -> String { translated(context, StockMessage::ReplyNoun).await @@ -1334,26 +1309,6 @@ impl Context { Ok(()) } - /// Returns a stock message saying that protection status has changed. - pub(crate) async fn stock_protection_msg( - &self, - protect: ProtectionStatus, - contact_id: Option, - ) -> String { - match protect { - ProtectionStatus::Unprotected => { - if let Some(contact_id) = contact_id { - chat_protection_disabled(self, contact_id).await - } else { - // In a group chat, it's not possible to downgrade verification. - // In a 1:1 chat, the `contact_id` always has to be provided. - "[Error] No contact_id given".to_string() - } - } - ProtectionStatus::Protected => messages_e2e_encrypted(self).await, - } - } - pub(crate) async fn update_device_chats(&self) -> Result<()> { if self.get_config_bool(Config::Bot).await? { return Ok(()); diff --git a/src/sync.rs b/src/sync.rs index 1f17ff48c7..26ff540501 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -253,13 +253,19 @@ impl Context { /// If an error is returned, the caller shall not try over because some sync items could be /// already executed. Sync items are considered independent and executed in the given order but /// regardless of whether executing of the previous items succeeded. - pub(crate) async fn execute_sync_items(&self, items: &SyncItems) { + pub(crate) async fn execute_sync_items(&self, items: &SyncItems, timestamp_sent: i64) { info!(self, "executing {} sync item(s)", items.items.len()); for item in &items.items { + // Limit the timestamp to ensure it is not in the future. + // + // `sent_timestamp` should be already corrected + // if the `Date` header is in the future. + let timestamp = std::cmp::min(item.timestamp, timestamp_sent); + match &item.data { SyncDataOrUnknown::SyncData(data) => match data { - AddQrToken(token) => self.add_qr_token(token).await, - DeleteQrToken(token) => self.delete_qr_token(token).await, + AddQrToken(token) => self.add_qr_token(token, timestamp).await, + DeleteQrToken(token) => self.delete_qr_token(token, timestamp).await, AlterChat { id, action } => self.sync_alter_chat(id, action).await, SyncData::Config { key, val } => self.sync_config(key, val).await, SyncData::SaveMessage { src, dest } => self.save_message(src, dest).await, @@ -284,21 +290,28 @@ impl Context { } } - async fn add_qr_token(&self, token: &QrTokenData) -> Result<()> { + async fn add_qr_token(&self, token: &QrTokenData, timestamp: i64) -> Result<()> { let grpid = token.grpid.as_deref(); - token::save(self, Namespace::InviteNumber, grpid, &token.invitenumber).await?; - token::save(self, Namespace::Auth, grpid, &token.auth).await?; + token::save( + self, + Namespace::InviteNumber, + grpid, + &token.invitenumber, + timestamp, + ) + .await?; + token::save(self, Namespace::Auth, grpid, &token.auth, timestamp).await?; Ok(()) } - async fn delete_qr_token(&self, token: &QrTokenData) -> Result<()> { + async fn delete_qr_token(&self, token: &QrTokenData, timestamp: i64) -> Result<()> { self.sql .execute( "DELETE FROM tokens WHERE foreign_key IN (SELECT foreign_key FROM tokens - WHERE token=? OR token=?)", - (&token.invitenumber, &token.auth), + WHERE token=? OR token=? AND timestamp <= ?)", + (&token.invitenumber, &token.auth, timestamp), ) .await?; Ok(()) @@ -339,7 +352,7 @@ mod tests { use anyhow::bail; use super::*; - use crate::chat::{Chat, ProtectionStatus, remove_contact_from_chat}; + use crate::chat::{Chat, remove_contact_from_chat}; use crate::chatlist::Chatlist; use crate::contact::{Contact, Origin}; use crate::securejoin::get_securejoin_qr; @@ -550,6 +563,7 @@ mod tests { assert!(!token::exists(&t, Namespace::Auth, "yip-auth").await?); + let timestamp_sent = time(); let sync_items = t .parse_sync_items( r#"{"items":[ @@ -564,7 +578,7 @@ mod tests { .to_string(), ) ?; - t.execute_sync_items(&sync_items).await; + t.execute_sync_items(&sync_items, timestamp_sent).await; assert!( Contact::lookup_id_by_addr(&t, "bob@example.net", Origin::Unknown) @@ -714,8 +728,7 @@ mod tests { let mut tcm = TestContextManager::new(); let alice = &tcm.alice().await; alice.set_config_bool(Config::SyncMsgs, true).await?; - let alice_chatid = - chat::create_group_chat(alice, ProtectionStatus::Protected, "the chat").await?; + let alice_chatid = chat::create_group(alice, "the chat").await?; let qr = get_securejoin_qr(alice, Some(alice_chatid)).await?; // alice2 syncs the QR code token. diff --git a/src/test_utils.rs b/src/test_utils.rs index ca2d931acf..91904333aa 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -21,8 +21,7 @@ use tokio::runtime::Handle; use tokio::{fs, task}; use crate::chat::{ - self, Chat, ChatId, ChatIdBlocked, MessageListOptions, ProtectionStatus, - add_to_chat_contacts_table, create_group_chat, + self, Chat, ChatId, ChatIdBlocked, MessageListOptions, add_to_chat_contacts_table, create_group, }; use crate::chatlist::Chatlist; use crate::config::Config; @@ -84,6 +83,7 @@ impl TestContextManager { pub async fn alice(&mut self) -> TestContext { TestContext::builder() .configure_alice() + .with_id_offset(1000) .with_log_sink(self.log_sink.clone()) .build(Some(&mut self.used_names)) .await @@ -92,6 +92,7 @@ impl TestContextManager { pub async fn bob(&mut self) -> TestContext { TestContext::builder() .configure_bob() + .with_id_offset(2000) .with_log_sink(self.log_sink.clone()) .build(Some(&mut self.used_names)) .await @@ -100,6 +101,7 @@ impl TestContextManager { pub async fn charlie(&mut self) -> TestContext { TestContext::builder() .configure_charlie() + .with_id_offset(3000) .with_log_sink(self.log_sink.clone()) .build(Some(&mut self.used_names)) .await @@ -108,6 +110,7 @@ impl TestContextManager { pub async fn dom(&mut self) -> TestContext { TestContext::builder() .configure_dom() + .with_id_offset(4000) .with_log_sink(self.log_sink.clone()) .build(Some(&mut self.used_names)) .await @@ -116,6 +119,7 @@ impl TestContextManager { pub async fn elena(&mut self) -> TestContext { TestContext::builder() .configure_elena() + .with_id_offset(5000) .with_log_sink(self.log_sink.clone()) .build(Some(&mut self.used_names)) .await @@ -124,6 +128,7 @@ impl TestContextManager { pub async fn fiona(&mut self) -> TestContext { TestContext::builder() .configure_fiona() + .with_id_offset(6000) .with_log_sink(self.log_sink.clone()) .build(Some(&mut self.used_names)) .await @@ -263,6 +268,11 @@ pub struct TestContextBuilder { /// so the caller should store the LogSink elsewhere to /// prevent it from being dropped immediately. log_sink: Option, + + /// Offset for chat-,message-,contact ids. + /// + /// This makes tests fail where ids from different accounts were mixed up. + id_offset: Option, } impl TestContextBuilder { @@ -328,6 +338,14 @@ impl TestContextBuilder { self } + /// Adds an offset for chat-, message-, contact IDs. + /// + /// This makes it harder to accidentally mix up IDs from different accounts. + pub fn with_id_offset(mut self, offset: u32) -> Self { + self.id_offset = Some(offset); + self + } + /// Builds the [`TestContext`]. pub async fn build(self, used_names: Option<&mut BTreeSet>) -> TestContext { if let Some(key_pair) = self.key_pair { @@ -360,6 +378,22 @@ impl TestContextBuilder { key::store_self_keypair(&test_context, &key_pair) .await .expect("Failed to save key"); + + if let Some(offset) = self.id_offset { + test_context + .ctx + .sql + .execute( + "UPDATE sqlite_sequence SET seq = ? + WHERE name = 'contacts' + OR name = 'chats' + OR name = 'msgs'", + (offset,), + ) + .await + .expect("Failed set id offset"); + } + test_context } else { TestContext::new_internal(None, self.log_sink).await @@ -409,21 +443,33 @@ impl TestContext { /// /// This is a shortcut which configures alice@example.org with a fixed key. pub async fn new_alice() -> Self { - Self::builder().configure_alice().build(None).await + Self::builder() + .configure_alice() + .with_id_offset(11000) + .build(None) + .await } /// Creates a new configured [`TestContext`]. /// /// This is a shortcut which configures bob@example.net with a fixed key. pub async fn new_bob() -> Self { - Self::builder().configure_bob().build(None).await + Self::builder() + .configure_bob() + .with_id_offset(12000) + .build(None) + .await } /// Creates a new configured [`TestContext`]. /// /// This is a shortcut which configures fiona@example.net with a fixed key. pub async fn new_fiona() -> Self { - Self::builder().configure_fiona().build(None).await + Self::builder() + .configure_fiona() + .with_id_offset(13000) + .build(None) + .await } /// Print current chat state. @@ -1013,7 +1059,7 @@ impl TestContext { }; writeln!( res, - "{}#{}: {} [{}]{}{}{} {}", + "{}#{}: {} [{}]{}{}{}", sel_chat.typ, sel_chat.get_id(), sel_chat.get_name(), @@ -1031,11 +1077,6 @@ impl TestContext { }, _ => "".to_string(), }, - if sel_chat.is_protected() { - "πŸ›‘οΈ" - } else { - "" - }, ) .unwrap(); @@ -1066,11 +1107,10 @@ impl TestContext { pub async fn create_group_with_members( &self, - protect: ProtectionStatus, chat_name: &str, members: &[&TestContext], ) -> ChatId { - let chat_id = create_group_chat(self, protect, chat_name).await.unwrap(); + let chat_id = create_group(self, chat_name).await.unwrap(); let mut to_add = vec![]; for member in members { let contact_id = self.add_or_lookup_contact_id(member).await; @@ -1630,4 +1670,32 @@ mod tests { let runtime = tokio::runtime::Runtime::new().expect("unable to create tokio runtime"); runtime.block_on(TestContext::new()); } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_id_offset() { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + let fiona = tcm.fiona().await; + + // chat ids + let alice_bob_chat = alice.create_chat(&bob).await; + let bob_alice_chat = bob.create_chat(&alice).await; + assert_ne!(alice_bob_chat.id, bob_alice_chat.id); + + // contact ids + let alice_fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await; + let fiona_fiona_contact_id = bob.add_or_lookup_contact_id(&fiona).await; + assert_ne!(alice_fiona_contact_id, fiona_fiona_contact_id); + + // message ids + let alice_group_id = alice + .create_group_with_members("test group", &[&bob, &fiona]) + .await; + let alice_sent_msg = alice.send_text(alice_group_id, "testing").await; + let bob_received_id = bob.recv_msg(&alice_sent_msg).await; + assert_ne!(alice_sent_msg.sender_msg_id, bob_received_id.id); + let fiona_received_id = fiona.recv_msg(&alice_sent_msg).await; + assert_ne!(bob_received_id.id, fiona_received_id.id); + } } diff --git a/src/tests/aeap.rs b/src/tests/aeap.rs index 55c5342f9b..f3270f5310 100644 --- a/src/tests/aeap.rs +++ b/src/tests/aeap.rs @@ -9,7 +9,7 @@ use anyhow::Result; -use crate::chat::{self, Chat, ChatId, ProtectionStatus}; +use crate::chat::{self, Chat, ChatId}; use crate::contact::{Contact, ContactId}; use crate::message::Message; use crate::receive_imf::receive_imf; @@ -90,24 +90,12 @@ async fn check_aeap_transition(chat_for_transition: ChatForTransition, verified: } let mut groups = vec![ - chat::create_group_chat(bob, chat::ProtectionStatus::Unprotected, "Group 0") - .await - .unwrap(), - chat::create_group_chat(bob, chat::ProtectionStatus::Unprotected, "Group 1") - .await - .unwrap(), + chat::create_group(bob, "Group 0").await.unwrap(), + chat::create_group(bob, "Group 1").await.unwrap(), ]; if verified { - groups.push( - chat::create_group_chat(bob, chat::ProtectionStatus::Protected, "Group 2") - .await - .unwrap(), - ); - groups.push( - chat::create_group_chat(bob, chat::ProtectionStatus::Protected, "Group 3") - .await - .unwrap(), - ); + groups.push(chat::create_group(bob, "Group 2").await.unwrap()); + groups.push(chat::create_group(bob, "Group 3").await.unwrap()); } let alice_contact = bob.add_or_lookup_contact_id(alice).await; @@ -201,8 +189,7 @@ async fn test_aeap_replay_attack() -> Result<()> { tcm.send_recv_accept(&alice, &bob, "Hi").await; tcm.send_recv(&bob, &alice, "Hi back").await; - let group = - chat::create_group_chat(&bob, chat::ProtectionStatus::Unprotected, "Group 0").await?; + let group = chat::create_group(&bob, "Group 0").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; @@ -217,10 +204,12 @@ async fn test_aeap_replay_attack() -> Result<()> { // Fiona gets the message, replaces the From addr... let sent = sent .payload() - .replace("From: ", "From: ") - .replace("addr=alice@example.org;", "addr=fiona@example.net;"); + .replace("From: ", "From: "); sent.find("From: ").unwrap(); // Assert that it worked - sent.find("addr=fiona@example.net;").unwrap(); // Assert that it worked + + // Autocrypt header is protected, nothing to replace outside. + // In the signed part we cannot replace it without breaking the signature. + assert!(!sent.contains("addr=alice@example.org;")); tcm.section("Fiona replaced the From addr and forwards the message to Bob"); receive_imf(&bob, sent.as_bytes(), false).await?.unwrap(); @@ -243,16 +232,13 @@ async fn test_write_to_alice_after_aeap() -> Result<()> { let alice = &tcm.alice().await; let bob = &tcm.bob().await; - let alice_grp_id = chat::create_group_chat(alice, ProtectionStatus::Protected, "Group").await?; + let alice_grp_id = chat::create_group(alice, "Group").await?; let qr = get_securejoin_qr(alice, Some(alice_grp_id)).await?; tcm.exec_securejoin_qr(bob, alice, &qr).await; let bob_alice_contact = bob.add_or_lookup_contact(alice).await; assert!(bob_alice_contact.is_verified(bob).await?); let bob_alice_chat = bob.create_chat(alice).await; - assert!(bob_alice_chat.is_protected()); - let bob_unprotected_grp_id = bob - .create_group_with_members(ProtectionStatus::Unprotected, "Group", &[alice]) - .await; + let bob_unprotected_grp_id = bob.create_group_with_members("Group", &[alice]).await; tcm.change_addr(alice, "alice@someotherdomain.xyz").await; let sent = alice.send_text(alice_grp_id, "Hello!").await; @@ -260,7 +246,6 @@ async fn test_write_to_alice_after_aeap() -> Result<()> { 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()); chat::send_msg(bob, bob_alice_chat.id, &mut msg).await?; diff --git a/src/tests/verified_chats.rs b/src/tests/verified_chats.rs index f1e6bba486..c120d73baa 100644 --- a/src/tests/verified_chats.rs +++ b/src/tests/verified_chats.rs @@ -2,9 +2,7 @@ use anyhow::Result; use pretty_assertions::assert_eq; use crate::chat::resend_msgs; -use crate::chat::{ - self, Chat, ProtectionStatus, add_contact_to_chat, remove_contact_from_chat, send_msg, -}; +use crate::chat::{self, Chat, add_contact_to_chat, remove_contact_from_chat, send_msg}; use crate::config::Config; use crate::constants::Chattype; use crate::contact::{Contact, ContactId}; @@ -38,8 +36,8 @@ async fn check_verified_oneonone_chat_protection_not_broken(by_classical_email: tcm.execute_securejoin(&alice, &bob).await; - assert_verified(&alice, &bob, ProtectionStatus::Protected).await; - assert_verified(&bob, &alice, ProtectionStatus::Protected).await; + assert_verified(&alice, &bob).await; + assert_verified(&bob, &alice).await; if by_classical_email { tcm.section("Bob uses a classical MUA to send a message to Alice"); @@ -59,7 +57,7 @@ async fn check_verified_oneonone_chat_protection_not_broken(by_classical_email: .unwrap(); let contact = alice.add_or_lookup_contact(&bob).await; assert_eq!(contact.is_verified(&alice).await.unwrap(), true); - assert_verified(&alice, &bob, ProtectionStatus::Protected).await; + assert_verified(&alice, &bob).await; } else { tcm.section("Bob sets up another Delta Chat device"); let bob2 = tcm.unconfigured().await; @@ -71,7 +69,7 @@ async fn check_verified_oneonone_chat_protection_not_broken(by_classical_email: .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; + assert_verified(&alice, &bob).await; } tcm.section("Bob sends another message from DC"); @@ -79,7 +77,7 @@ async fn check_verified_oneonone_chat_protection_not_broken(by_classical_email: tcm.send_recv(&bob, &alice, "Using DC again").await; // Bob's chat is marked as verified again - assert_verified(&alice, &bob, ProtectionStatus::Protected).await; + assert_verified(&alice, &bob).await; } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -91,21 +89,17 @@ async fn test_create_verified_oneonone_chat() -> Result<()> { tcm.execute_securejoin(&alice, &bob).await; tcm.execute_securejoin(&bob, &fiona).await; - assert_verified(&alice, &bob, ProtectionStatus::Protected).await; - assert_verified(&bob, &alice, ProtectionStatus::Protected).await; - assert_verified(&bob, &fiona, ProtectionStatus::Protected).await; - assert_verified(&fiona, &bob, ProtectionStatus::Protected).await; + assert_verified(&alice, &bob).await; + assert_verified(&bob, &alice).await; + assert_verified(&bob, &fiona).await; + assert_verified(&fiona, &bob).await; let group_id = bob - .create_group_with_members( - ProtectionStatus::Protected, - "Group with everyone", - &[&alice, &fiona], - ) + .create_group_with_members("Group with everyone", &[&alice, &fiona]) .await; assert_eq!( get_chat_msg(&bob, group_id, 0, 1).await.get_info_type(), - SystemMessage::ChatProtectionEnabled + SystemMessage::ChatE2ee ); { @@ -117,7 +111,7 @@ async fn test_create_verified_oneonone_chat() -> Result<()> { get_chat_msg(&fiona, msg.chat_id, 0, 2) .await .get_info_type(), - SystemMessage::ChatProtectionEnabled + SystemMessage::ChatE2ee ); } @@ -125,26 +119,6 @@ async fn test_create_verified_oneonone_chat() -> Result<()> { let alice_fiona_contact = alice.add_or_lookup_contact(&fiona).await; assert!(alice_fiona_contact.is_verified(&alice).await.unwrap(),); - // Alice should have a hidden protected chat with Fiona - { - let chat = alice.get_chat(&fiona).await; - assert!(chat.is_protected()); - - let msg = get_chat_msg(&alice, chat.id, 0, 1).await; - let expected_text = stock_str::messages_e2e_encrypted(&alice).await; - assert_eq!(msg.text, expected_text); - } - - // Fiona should have a hidden protected chat with Alice - { - let chat = fiona.get_chat(&alice).await; - assert!(chat.is_protected()); - - let msg0 = get_chat_msg(&fiona, chat.id, 0, 1).await; - let expected_text = stock_str::messages_e2e_encrypted(&fiona).await; - assert_eq!(msg0.text, expected_text); - } - tcm.section("Fiona reinstalls DC"); drop(fiona); @@ -155,19 +129,12 @@ async fn test_create_verified_oneonone_chat() -> Result<()> { tcm.send_recv(&fiona_new, &alice, "I have a new device") .await; - // Alice gets a new unprotected chat with new Fiona contact. + // Alice gets a new chat with new Fiona contact. { let chat = alice.get_chat(&fiona_new).await; - assert!(!chat.is_protected()); let msg = get_chat_msg(&alice, chat.id, 1, E2EE_INFO_MSGS + 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()); } Ok(()) @@ -180,7 +147,7 @@ async fn test_missing_key_reexecute_securejoin() -> Result<()> { let bob = &tcm.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()); + assert!(chat.can_send(bob).await?); bob.sql .execute( "DELETE FROM public_keys WHERE fingerprint=?", @@ -191,43 +158,12 @@ async fn test_missing_key_reexecute_securejoin() -> Result<()> { .hex(),), ) .await?; - let chat_id = tcm.execute_securejoin(bob, alice).await; let chat = Chat::load_from_db(bob, chat_id).await?; - assert!(chat.is_protected()); - Ok(()) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_create_unverified_oneonone_chat() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - - // A chat with an unknown contact should be created unprotected - let chat = alice.create_chat(&bob).await; - assert!(!chat.is_protected()); - - receive_imf( - &alice, - b"From: Bob \n\ - To: alice@example.org\n\ - Message-ID: <1234-2@example.org>\n\ - \n\ - hello\n", - false, - ) - .await?; - - chat.id.delete(&alice).await.unwrap(); - // Now Bob is a known contact, new chats should still be created unprotected - let chat = alice.create_chat(&bob).await; - assert!(!chat.is_protected()); + assert!(!chat.can_send(bob).await?); - tcm.send_recv(&bob, &alice, "hi").await; - chat.id.delete(&alice).await.unwrap(); - // Now we have a public key, new chats should still be created unprotected - let chat = alice.create_chat(&bob).await; - assert!(!chat.is_protected()); + let chat_id = tcm.execute_securejoin(bob, alice).await; + let chat = Chat::load_from_db(bob, chat_id).await?; + assert!(chat.can_send(bob).await?); Ok(()) } @@ -245,7 +181,6 @@ async fn test_degrade_verified_oneonone_chat() -> Result<()> { mark_as_verified(&alice, &bob).await; let alice_chat = alice.create_chat(&bob).await; - assert!(alice_chat.is_protected()); receive_imf( &alice, @@ -261,7 +196,7 @@ async fn test_degrade_verified_oneonone_chat() -> Result<()> { let msg0 = get_chat_msg(&alice, alice_chat.id, 0, 1).await; let enabled = stock_str::messages_e2e_encrypted(&alice).await; assert_eq!(msg0.text, enabled); - assert_eq!(msg0.param.get_cmd(), SystemMessage::ChatProtectionEnabled); + assert_eq!(msg0.param.get_cmd(), SystemMessage::ChatE2ee); let email_chat = alice.get_email_chat(&bob).await; assert!(!email_chat.is_encrypted(&alice).await?); @@ -369,7 +304,7 @@ async fn test_mdn_doesnt_disable_verification() -> Result<()> { let body = rendered_msg.message; receive_imf(&alice, body.as_bytes(), false).await.unwrap(); - assert_verified(&alice, &bob, ProtectionStatus::Protected).await; + assert_verified(&alice, &bob).await; Ok(()) } @@ -384,7 +319,7 @@ async fn test_outgoing_mua_msg() -> Result<()> { mark_as_verified(&bob, &alice).await; tcm.send_recv_accept(&bob, &alice, "Heyho from DC").await; - assert_verified(&alice, &bob, ProtectionStatus::Protected).await; + assert_verified(&alice, &bob).await; let sent = receive_imf( &alice, @@ -497,7 +432,7 @@ async fn test_message_from_old_dc_setup() -> Result<()> { mark_as_verified(bob, alice).await; tcm.send_recv(bob, alice, "Now i have it!").await; - assert_verified(alice, bob, ProtectionStatus::Protected).await; + assert_verified(alice, bob).await; let msg = alice.recv_msg(&sent_old).await; assert!(msg.get_showpadlock()); @@ -506,8 +441,6 @@ 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_chat(bob).await; - assert!(chat.is_protected()); Ok(()) } @@ -528,7 +461,7 @@ async fn test_verify_then_verify_again() -> Result<()> { mark_as_verified(&bob, &alice).await; alice.create_chat(&bob).await; - assert_verified(&alice, &bob, ProtectionStatus::Protected).await; + assert_verified(&alice, &bob).await; tcm.section("Bob reinstalls DC"); drop(bob); @@ -537,42 +470,39 @@ async fn test_verify_then_verify_again() -> Result<()> { e2ee::ensure_secret_key_exists(&bob_new).await?; tcm.execute_securejoin(&bob_new, &alice).await; - assert_verified(&alice, &bob_new, ProtectionStatus::Protected).await; + assert_verified(&alice, &bob_new).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. +/// Tests that on the second device of a group creator the first message is +/// `SystemMessage::ChatE2ee` and the second one is the message populating the group. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_create_protected_grp_multidev() -> Result<()> { +async fn test_create_grp_multidev() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = &tcm.alice().await; let alice1 = &tcm.alice().await; - let group_id = alice - .create_group_with_members(ProtectionStatus::Protected, "Group", &[]) - .await; + let group_id = alice.create_group_with_members("Group", &[]).await; assert_eq!( get_chat_msg(alice, group_id, 0, 1).await.get_info_type(), - SystemMessage::ChatProtectionEnabled + SystemMessage::ChatE2ee ); let sent = alice.send_text(group_id, "Hey").await; // This time shift is necessary to reproduce the bug when the original message is sorted over - // the "protection enabled" message so that these messages have different timestamps. + // the "Messages are end-to-end encrypted" message so that these messages have different timestamps. SystemTime::shift(std::time::Duration::from_secs(3600)); let msg = alice1.recv_msg(&sent).await; let group1 = Chat::load_from_db(alice1, msg.chat_id).await?; assert_eq!(group1.get_type(), Chattype::Group); - assert!(group1.is_protected()); assert_eq!( chat::get_chat_contacts(alice1, group1.id).await?, vec![ContactId::SELF] ); assert_eq!( get_chat_msg(alice1, group1.id, 0, 2).await.get_info_type(), - SystemMessage::ChatProtectionEnabled + SystemMessage::ChatE2ee ); assert_eq!(get_chat_msg(alice1, group1.id, 1, 2).await.id, msg.id); @@ -592,10 +522,8 @@ async fn test_verified_member_added_reordering() -> Result<()> { tcm.execute_securejoin(bob, alice).await; tcm.execute_securejoin(fiona, alice).await; - // Alice creates protected group with Bob. - let alice_chat_id = alice - .create_group_with_members(ProtectionStatus::Protected, "Group", &[bob]) - .await; + // Alice creates a group with Bob. + let alice_chat_id = alice.create_group_with_members("Group", &[bob]).await; let alice_sent_group_promotion = alice.send_text(alice_chat_id, "I created a group").await; let msg = bob.recv_msg(&alice_sent_group_promotion).await; let bob_chat_id = msg.chat_id; @@ -614,15 +542,13 @@ async fn test_verified_member_added_reordering() -> Result<()> { // "Member added" message, so unverified group is created. let fiona_received_message = fiona.recv_msg(&bob_sent_message).await; let fiona_chat = Chat::load_from_db(fiona, fiona_received_message.chat_id).await?; + assert!(!fiona_chat.can_send(fiona).await?); assert_eq!(fiona_received_message.get_text(), "Hi"); - assert_eq!(fiona_chat.is_protected(), false); // Fiona receives late "Member added" message // and the chat becomes protected. fiona.recv_msg(&alice_sent_member_added).await; - let fiona_chat = Chat::load_from_db(fiona, fiona_received_message.chat_id).await?; - assert_eq!(fiona_chat.is_protected(), true); Ok(()) } @@ -665,9 +591,7 @@ async fn test_verified_lost_member_added() -> Result<()> { tcm.execute_securejoin(bob, alice).await; tcm.execute_securejoin(fiona, alice).await; - let alice_chat_id = alice - .create_group_with_members(ProtectionStatus::Protected, "Group", &[bob]) - .await; + let alice_chat_id = alice.create_group_with_members("Group", &[bob]).await; let alice_sent = alice.send_text(alice_chat_id, "Hi!").await; let bob_chat_id = bob.recv_msg(&alice_sent).await.chat_id; assert_eq!(chat::get_chat_contacts(bob, bob_chat_id).await?.len(), 2); @@ -728,9 +652,7 @@ async fn test_verified_chat_editor_reordering() -> Result<()> { tcm.execute_securejoin(alice, bob).await; tcm.section("Alice creates a protected group with Bob"); - let alice_chat_id = alice - .create_group_with_members(ProtectionStatus::Protected, "Group", &[bob]) - .await; + let alice_chat_id = alice.create_group_with_members("Group", &[bob]).await; let alice_sent = alice.send_text(alice_chat_id, "Hi!").await; let bob_chat_id = bob.recv_msg(&alice_sent).await.chat_id; @@ -811,7 +733,7 @@ async fn test_no_reverification() -> Result<()> { tcm.section("Alice creates a protected group with Bob, Charlie and Fiona"); let alice_chat_id = alice - .create_group_with_members(ProtectionStatus::Protected, "Group", &[bob, charlie, fiona]) + .create_group_with_members("Group", &[bob, charlie, fiona]) .await; let alice_sent = alice.send_text(alice_chat_id, "Hi!").await; let bob_rcvd_msg = bob.recv_msg(&alice_sent).await; @@ -859,15 +781,47 @@ async fn test_no_reverification() -> Result<()> { Ok(()) } -// ============== Helper Functions ============== +/// Tests that if our second device observes +/// us gossiping a verification, +/// it is not treated as direct verification. +/// +/// Direct verifications should only happen +/// as a result of SecureJoin. +/// If we see our second device gossiping +/// a verification of some contact, +/// it may be indirect verification, +/// so we should mark the contact as verified, +/// but with unknown verifier. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_no_direct_verification_via_bcc() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let alice2 = &tcm.alice().await; + let bob = &tcm.bob().await; -async fn assert_verified(this: &TestContext, other: &TestContext, protected: ProtectionStatus) { - let contact = this.add_or_lookup_contact(other).await; - assert_eq!(contact.is_verified(this).await.unwrap(), true); + mark_as_verified(alice, bob).await; + + let alice_chat_id = alice.create_chat_id(bob).await; + let alice_sent_msg = alice.send_text(alice_chat_id, "Hello!").await; + alice2.recv_msg(&alice_sent_msg).await; - let chat = this.get_chat(other).await; + // Alice 2 observes Alice 1 gossiping verification for Bob. + // Alice 2 does not know if Alice 1 has verified Bob directly though. + let alice2_bob_contact = alice2.add_or_lookup_contact(bob).await; + assert_eq!(alice2_bob_contact.is_verified(alice2).await?, true); + + // There is some verifier, but it is unknown to Alice's second device. assert_eq!( - chat.is_protected(), - protected == ProtectionStatus::Protected + alice2_bob_contact.get_verifier_id(alice2).await?, + Some(None) ); + + Ok(()) +} + +// ============== Helper Functions ============== + +async fn assert_verified(this: &TestContext, other: &TestContext) { + let contact = this.add_or_lookup_contact(other).await; + assert_eq!(contact.is_verified(this).await.unwrap(), true); } diff --git a/src/token.rs b/src/token.rs index 7846e1e59a..1a189a151f 100644 --- a/src/token.rs +++ b/src/token.rs @@ -28,12 +28,13 @@ pub async fn save( namespace: Namespace, foreign_key: Option<&str>, token: &str, + timestamp: i64, ) -> Result<()> { context .sql .execute( "INSERT INTO tokens (namespc, foreign_key, token, timestamp) VALUES (?, ?, ?, ?)", - (namespace, foreign_key.unwrap_or(""), token, time()), + (namespace, foreign_key.unwrap_or(""), token, timestamp), ) .await?; Ok(()) @@ -71,7 +72,8 @@ pub async fn lookup_or_new( } let token = create_id(); - save(context, namespace, foreign_key, &token).await?; + let timestamp = time(); + save(context, namespace, foreign_key, &token, timestamp).await?; Ok(token) } @@ -86,24 +88,6 @@ pub async fn exists(context: &Context, namespace: Namespace, token: &str) -> Res Ok(exists) } -/// Looks up foreign key by auth token. -/// -/// Returns None if auth token is not valid. -/// Returns an empty string if the token corresponds to "setup contact" rather than group join. -pub async fn auth_foreign_key(context: &Context, token: &str) -> Result> { - context - .sql - .query_row_optional( - "SELECT foreign_key FROM tokens WHERE namespc=? AND token=?", - (Namespace::Auth, token), - |row| { - let foreign_key: String = row.get(0)?; - Ok(foreign_key) - }, - ) - .await -} - /// Resets all tokens corresponding to the `foreign_key`. /// /// `foreign_key` is a group ID to reset all group tokens diff --git a/src/tools.rs b/src/tools.rs index 59cca8d158..e7dc108cfb 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -753,6 +753,14 @@ pub(crate) fn buf_decompress(buf: &[u8]) -> Result> { Ok(mem::take(decompressor.get_mut())) } +/// Returns the given `&str` if already lowercased to avoid allocation, otherwise lowercases it. +pub(crate) fn to_lowercase(s: &str) -> Cow<'_, str> { + match s.chars().all(char::is_lowercase) { + true => Cow::Borrowed(s), + false => Cow::Owned(s.to_lowercase()), + } +} + /// Increments `*t` and checks that it equals to `expected` after that. pub(crate) fn inc_and_check( t: &mut T, diff --git a/src/webxdc/maps_integration.rs b/src/webxdc/maps_integration.rs index 7f398e6e06..3ee931a599 100644 --- a/src/webxdc/maps_integration.rs +++ b/src/webxdc/maps_integration.rs @@ -169,7 +169,7 @@ pub(crate) async fn intercept_get_updates( #[cfg(test)] mod tests { - use crate::chat::{ChatId, ProtectionStatus, create_group_chat}; + use crate::chat::{ChatId, create_group}; use crate::chatlist::Chatlist; use crate::contact::Contact; use crate::message::Message; @@ -231,7 +231,7 @@ mod tests { assert_eq!(msg.chat_id, bob_chat_id); // Integrate Webxdc into another group - let group_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let group_id = create_group(&t, "foo").await?; let integration_id = t.init_webxdc_integration(Some(group_id)).await?.unwrap(); let locations = location::get_range(&t, Some(group_id), None, 0, 0).await?; diff --git a/src/webxdc/webxdc_tests.rs b/src/webxdc/webxdc_tests.rs index d08ad2b301..cc205f3fc9 100644 --- a/src/webxdc/webxdc_tests.rs +++ b/src/webxdc/webxdc_tests.rs @@ -5,8 +5,8 @@ use serde_json::json; use super::*; use crate::chat::{ - ChatId, ProtectionStatus, add_contact_to_chat, create_broadcast, create_group_chat, - forward_msgs, remove_contact_from_chat, resend_msgs, send_msg, send_text_msg, + ChatId, add_contact_to_chat, create_broadcast, create_group, forward_msgs, + remove_contact_from_chat, resend_msgs, send_msg, send_text_msg, }; use crate::chatlist::Chatlist; use crate::config::Config; @@ -78,7 +78,7 @@ async fn send_webxdc_instance(t: &TestContext, chat_id: ChatId) -> Result Result<()> { let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let chat_id = create_group(&t, "foo").await?; // send as .xdc file let instance = send_webxdc_instance(&t, chat_id).await?; @@ -97,7 +97,7 @@ async fn test_send_webxdc_instance() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_send_invalid_webxdc() -> Result<()> { let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let chat_id = create_group(&t, "foo").await?; // sending invalid .xdc as file is possible, but must not result in Viewtype::Webxdc let mut instance = create_webxdc_instance( @@ -126,7 +126,7 @@ async fn test_send_invalid_webxdc() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_set_draft_invalid_webxdc() -> Result<()> { let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let chat_id = create_group(&t, "foo").await?; let mut instance = create_webxdc_instance( &t, @@ -143,7 +143,7 @@ async fn test_set_draft_invalid_webxdc() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_send_special_webxdc_format() -> Result<()> { let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let chat_id = create_group(&t, "foo").await?; // chess.xdc is failing for some zip-versions, see #3476, if we know more details about why, we can have a nicer name for the test :) let mut instance = create_webxdc_instance( @@ -164,7 +164,7 @@ async fn test_send_special_webxdc_format() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_forward_webxdc_instance() -> Result<()> { let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let chat_id = create_group(&t, "foo").await?; let instance = send_webxdc_instance(&t, chat_id).await?; t.send_webxdc_status_update( instance.id, @@ -213,7 +213,7 @@ async fn test_resend_webxdc_instance_and_info() -> Result<()> { // Alice uses webxdc in a group alice.set_config_bool(Config::BccSelf, false).await?; - let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?; + let alice_grp = create_group(&alice, "grp").await?; let alice_instance = send_webxdc_instance(&alice, alice_grp).await?; assert_eq!(alice_grp.get_msg_cnt(&alice).await?, 2); alice @@ -395,7 +395,7 @@ async fn test_webxdc_update_for_not_downloaded_instance() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_delete_webxdc_instance() -> Result<()> { let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let chat_id = create_group(&t, "foo").await?; let instance = send_webxdc_instance(&t, chat_id).await?; let now = tools::time(); t.receive_status_update( @@ -428,7 +428,7 @@ async fn test_delete_webxdc_instance() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_delete_chat_with_webxdc() -> Result<()> { let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let chat_id = create_group(&t, "foo").await?; let instance = send_webxdc_instance(&t, chat_id).await?; let now = tools::time(); t.receive_status_update( @@ -461,7 +461,7 @@ async fn test_delete_chat_with_webxdc() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_delete_webxdc_draft() -> Result<()> { let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let chat_id = create_group(&t, "foo").await?; let mut instance = create_webxdc_instance( &t, @@ -498,7 +498,7 @@ async fn test_delete_webxdc_draft() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_create_status_update_record() -> Result<()> { let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let chat_id = create_group(&t, "foo").await?; let instance = send_webxdc_instance(&t, chat_id).await?; assert_eq!( @@ -633,7 +633,7 @@ async fn test_create_status_update_record() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_receive_status_update() -> Result<()> { let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let chat_id = create_group(&t, "foo").await?; let instance = send_webxdc_instance(&t, chat_id).await?; let now = tools::time(); @@ -904,7 +904,7 @@ async fn test_send_big_webxdc_status_update() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_render_webxdc_status_update_object() -> Result<()> { let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat").await?; + let chat_id = create_group(&t, "a chat").await?; let mut instance = create_webxdc_instance( &t, "minimal.xdc", @@ -932,7 +932,7 @@ async fn test_render_webxdc_status_update_object() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_render_webxdc_status_update_object_range() -> Result<()> { let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat").await?; + let chat_id = create_group(&t, "a chat").await?; let instance = send_webxdc_instance(&t, chat_id).await?; t.send_webxdc_status_update(instance.id, r#"{"payload": 1}"#) .await?; @@ -979,7 +979,7 @@ async fn test_render_webxdc_status_update_object_range() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_pop_status_update() -> Result<()> { let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat").await?; + let chat_id = create_group(&t, "a chat").await?; let instance1 = send_webxdc_instance(&t, chat_id).await?; let instance2 = send_webxdc_instance(&t, chat_id).await?; let instance3 = send_webxdc_instance(&t, chat_id).await?; @@ -1109,7 +1109,7 @@ async fn test_draft_and_send_webxdc_status_update() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_send_webxdc_status_update_to_non_webxdc() -> Result<()> { let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let chat_id = create_group(&t, "foo").await?; let msg_id = send_text_msg(&t, chat_id, "ho!".to_string()).await?; assert!( t.send_webxdc_status_update(msg_id, r#"{"foo":"bar"}"#) @@ -1122,7 +1122,7 @@ async fn test_send_webxdc_status_update_to_non_webxdc() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_get_webxdc_blob() -> Result<()> { let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let chat_id = create_group(&t, "foo").await?; let instance = send_webxdc_instance(&t, chat_id).await?; let buf = instance.get_webxdc_blob(&t, "index.html").await?; @@ -1141,7 +1141,7 @@ async fn test_get_webxdc_blob() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_get_webxdc_blob_default_icon() -> Result<()> { let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let chat_id = create_group(&t, "foo").await?; let instance = send_webxdc_instance(&t, chat_id).await?; let buf = instance.get_webxdc_blob(&t, WEBXDC_DEFAULT_ICON).await?; @@ -1153,7 +1153,7 @@ async fn test_get_webxdc_blob_default_icon() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_get_webxdc_blob_with_absolute_paths() -> Result<()> { let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let chat_id = create_group(&t, "foo").await?; let instance = send_webxdc_instance(&t, chat_id).await?; let buf = instance.get_webxdc_blob(&t, "/index.html").await?; @@ -1166,7 +1166,7 @@ async fn test_get_webxdc_blob_with_absolute_paths() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_get_webxdc_blob_with_subdirs() -> Result<()> { let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let chat_id = create_group(&t, "foo").await?; let mut instance = create_webxdc_instance( &t, "some-files.xdc", @@ -1265,7 +1265,7 @@ async fn test_parse_webxdc_manifest_source_code_url() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_webxdc_min_api_too_large() -> Result<()> { let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?; + let chat_id = create_group(&t, "chat").await?; let mut instance = create_webxdc_instance( &t, "with-min-api-1001.xdc", @@ -1283,7 +1283,7 @@ async fn test_webxdc_min_api_too_large() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_get_webxdc_info() -> Result<()> { let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let chat_id = create_group(&t, "foo").await?; let instance = send_webxdc_instance(&t, chat_id).await?; let info = instance.get_webxdc_info(&t).await?; @@ -1363,7 +1363,7 @@ async fn test_get_webxdc_info() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_get_webxdc_self_addr() -> Result<()> { let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let chat_id = create_group(&t, "foo").await?; let instance = send_webxdc_instance(&t, chat_id).await?; let info1 = instance.get_webxdc_info(&t).await?; @@ -1601,7 +1601,7 @@ async fn test_webxdc_info_msg_cleanup_series() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_webxdc_info_msg_no_cleanup_on_interrupted_series() -> Result<()> { let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "c").await?; + let chat_id = create_group(&t, "c").await?; let instance = send_webxdc_instance(&t, chat_id).await?; t.send_webxdc_status_update(instance.id, r#"{"info":"i1", "payload":1}"#) @@ -1623,7 +1623,7 @@ async fn test_webxdc_no_internet_access() -> Result<()> { let t = TestContext::new_alice().await; let self_id = t.get_self_chat().await.id; let single_id = t.create_chat_with_contact("bob", "bob@e.com").await.id; - let group_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?; + let group_id = create_group(&t, "chat").await?; let broadcast_id = create_broadcast(&t, "Channel".to_string()).await?; for chat_id in [self_id, single_id, group_id, broadcast_id] { @@ -1655,7 +1655,7 @@ async fn test_webxdc_no_internet_access() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_webxdc_chatlist_summary() -> Result<()> { let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?; + let chat_id = create_group(&t, "chat").await?; let mut instance = create_webxdc_instance( &t, "with-minimal-manifest.xdc", @@ -1727,7 +1727,7 @@ async fn test_webxdc_reject_updates_from_non_groupmembers() -> Result<()> { let alice = &tcm.alice().await; let bob = &tcm.bob().await; let contact_bob = alice.add_or_lookup_contact_id(bob).await; - let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?; + let chat_id = create_group(alice, "Group").await?; add_contact_to_chat(alice, chat_id, contact_bob).await?; let instance = send_webxdc_instance(alice, chat_id).await?; bob.recv_msg(&alice.pop_sent_msg().await).await; @@ -1758,7 +1758,7 @@ async fn test_webxdc_reject_updates_from_non_groupmembers() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_webxdc_delete_event() -> Result<()> { let alice = TestContext::new_alice().await; - let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?; + let chat_id = create_group(&alice, "foo").await?; let instance = send_webxdc_instance(&alice, chat_id).await?; message::delete_msgs(&alice, &[instance.id]).await?; alice @@ -1912,7 +1912,7 @@ async fn test_webxdc_notify_one() -> Result<()> { let fiona = tcm.fiona().await; let grp_id = alice - .create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&bob, &fiona]) + .create_group_with_members("grp", &[&bob, &fiona]) .await; let alice_instance = send_webxdc_instance(&alice, grp_id).await?; let sent1 = alice.pop_sent_msg().await; @@ -1958,7 +1958,7 @@ async fn test_webxdc_notify_multiple() -> Result<()> { let fiona = tcm.fiona().await; let grp_id = alice - .create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&bob, &fiona]) + .create_group_with_members("grp", &[&bob, &fiona]) .await; let alice_instance = send_webxdc_instance(&alice, grp_id).await?; let sent1 = alice.pop_sent_msg().await; @@ -2001,9 +2001,7 @@ async fn test_webxdc_no_notify_self() -> Result<()> { let alice = tcm.alice().await; let alice2 = tcm.alice().await; - let grp_id = alice - .create_group_with_members(ProtectionStatus::Unprotected, "grp", &[]) - .await; + let grp_id = alice.create_group_with_members("grp", &[]).await; let alice_instance = send_webxdc_instance(&alice, grp_id).await?; let sent1 = alice.pop_sent_msg().await; let alice2_instance = alice2.recv_msg(&sent1).await; @@ -2043,7 +2041,7 @@ async fn test_webxdc_notify_all() -> Result<()> { let fiona = tcm.fiona().await; let grp_id = alice - .create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&bob, &fiona]) + .create_group_with_members("grp", &[&bob, &fiona]) .await; let alice_instance = send_webxdc_instance(&alice, grp_id).await?; let sent1 = alice.pop_sent_msg().await; @@ -2083,7 +2081,7 @@ async fn test_webxdc_notify_bob_and_all() -> Result<()> { let fiona = tcm.fiona().await; let grp_id = alice - .create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&bob, &fiona]) + .create_group_with_members("grp", &[&bob, &fiona]) .await; let alice_instance = send_webxdc_instance(&alice, grp_id).await?; let sent1 = alice.pop_sent_msg().await; @@ -2117,7 +2115,7 @@ async fn test_webxdc_notify_all_and_bob() -> Result<()> { let fiona = tcm.fiona().await; let grp_id = alice - .create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&bob, &fiona]) + .create_group_with_members("grp", &[&bob, &fiona]) .await; let alice_instance = send_webxdc_instance(&alice, grp_id).await?; let sent1 = alice.pop_sent_msg().await; @@ -2149,9 +2147,7 @@ async fn test_webxdc_href() -> Result<()> { let alice = tcm.alice().await; let bob = tcm.bob().await; - let grp_id = alice - .create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&bob]) - .await; + let grp_id = alice.create_group_with_members("grp", &[&bob]).await; let instance = send_webxdc_instance(&alice, grp_id).await?; let sent1 = alice.pop_sent_msg().await; @@ -2181,9 +2177,7 @@ async fn test_webxdc_href() -> Result<()> { async fn test_self_addr_consistency() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = &tcm.alice().await; - let alice_chat = alice - .create_group_with_members(ProtectionStatus::Unprotected, "No friends :(", &[]) - .await; + let alice_chat = alice.create_group_with_members("No friends :(", &[]).await; let mut instance = create_webxdc_instance( alice, "minimal.xdc", diff --git a/test-data/golden/chat_test_parallel_member_remove b/test-data/golden/chat_test_parallel_member_remove index c71bcf0d53..894f275de7 100644 --- a/test-data/golden/chat_test_parallel_member_remove +++ b/test-data/golden/chat_test_parallel_member_remove @@ -1,8 +1,8 @@ -Group#Chat#10: Group chat [3 member(s)] +Group#Chat#2001: Group chat [3 member(s)] -------------------------------------------------------------------------------- -Msg#10: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO] -Msg#11πŸ”’: (Contact#Contact#10): Hi! I created a group. [FRESH] -Msg#12πŸ”’: Me (Contact#Contact#Self): You left the group. [INFO] √ -Msg#13πŸ”’: (Contact#Contact#10): Member charlie@example.net added by alice@example.org. [FRESH][INFO] -Msg#14πŸ”’: (Contact#Contact#10): What a silence! [FRESH] +Msg#2001: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO] +Msg#2002πŸ”’: (Contact#Contact#2001): Hi! I created a group. [FRESH] +Msg#2003πŸ”’: Me (Contact#Contact#Self): You left the group. [INFO] √ +Msg#2004πŸ”’: (Contact#Contact#2001): Member charlie@example.net added by alice@example.org. [FRESH][INFO] +Msg#2005πŸ”’: (Contact#Contact#2001): What a silence! [FRESH] -------------------------------------------------------------------------------- diff --git a/test-data/golden/receive_imf_delayed_removal_is_ignored b/test-data/golden/receive_imf_delayed_removal_is_ignored index edcf03c1ec..3333fb8338 100644 --- a/test-data/golden/receive_imf_delayed_removal_is_ignored +++ b/test-data/golden/receive_imf_delayed_removal_is_ignored @@ -1,10 +1,10 @@ -Group#Chat#10: Group [5 member(s)] +Group#Chat#1001: Group [5 member(s)] -------------------------------------------------------------------------------- -Msg#10: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO] -Msg#11πŸ”’: Me (Contact#Contact#Self): populate √ -Msg#12: info (Contact#Contact#Info): Member dom@example.net added. [NOTICED][INFO] -Msg#13: info (Contact#Contact#Info): Member fiona@example.net removed. [NOTICED][INFO] -Msg#14πŸ”’: (Contact#Contact#10): Member elena@example.net added by bob@example.net. [FRESH][INFO] -Msg#15πŸ”’: Me (Contact#Contact#Self): You added member fiona@example.net. [INFO] o -Msg#16πŸ”’: (Contact#Contact#10): Member fiona@example.net removed by bob@example.net. [FRESH][INFO] +Msg#1001: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO] +Msg#1002πŸ”’: Me (Contact#Contact#Self): populate √ +Msg#1003: info (Contact#Contact#Info): Member dom@example.net added. [NOTICED][INFO] +Msg#1004: info (Contact#Contact#Info): Member fiona@example.net removed. [NOTICED][INFO] +Msg#1005πŸ”’: (Contact#Contact#1001): Member elena@example.net added by bob@example.net. [FRESH][INFO] +Msg#1006πŸ”’: Me (Contact#Contact#Self): You added member fiona@example.net. [INFO] o +Msg#1007πŸ”’: (Contact#Contact#1001): Member fiona@example.net removed by bob@example.net. [FRESH][INFO] -------------------------------------------------------------------------------- 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 abc38ad339..b833432873 100644 --- a/test-data/golden/receive_imf_older_message_from_2nd_device +++ b/test-data/golden/receive_imf_older_message_from_2nd_device @@ -1,5 +1,5 @@ -Single#Chat#10: bob@example.net [bob@example.net] Icon: 4138c52e5bc1c576cda7dd44d088c07.png +Single#Chat#1001: 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 √ +Msg#1001: Me (Contact#Contact#Self): We share this account √ +Msg#1002: 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 624838a43c..92bd8ac863 100644 --- a/test-data/golden/test_old_message_5 +++ b/test-data/golden/test_old_message_5 @@ -1,5 +1,5 @@ -Single#Chat#10: Bob [bob@example.net] Icon: 4138c52e5bc1c576cda7dd44d088c07.png +Single#Chat#11001: 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] +Msg#11001: Me (Contact#Contact#Self): Happy birthday, Bob! √ +Msg#11002: (Contact#Contact#11001): 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 06cecece6a..4b26297c2d 100644 --- a/test-data/golden/test_outgoing_encrypted_msg +++ b/test-data/golden/test_outgoing_encrypted_msg @@ -1,5 +1,5 @@ -Single#Chat#10: bob@example.net [KEY bob@example.net] πŸ›‘οΈ +Single#Chat#1001: bob@example.net [KEY bob@example.net] -------------------------------------------------------------------------------- -Msg#10: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO πŸ›‘οΈ] -Msg#11πŸ”’: Me (Contact#Contact#Self): Test – This is encrypted, signed, and has an Autocrypt Header without prefer-encrypt=mutual. √ +Msg#1001: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO] +Msg#1002πŸ”’: 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 5d4a0f2ee9..5940262b13 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: 4138c52e5bc1c576cda7dd44d088c07.png +Single#Chat#1002: bob@example.net [bob@example.net] Icon: 4138c52e5bc1c576cda7dd44d088c07.png -------------------------------------------------------------------------------- -Msg#12: Me (Contact#Contact#Self): One classical MUA message √ +Msg#1003: 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 1a128d5286..3c4c163f4b 100644 --- a/test-data/golden/test_outgoing_mua_msg_pgp +++ b/test-data/golden/test_outgoing_mua_msg_pgp @@ -1,6 +1,6 @@ -Single#Chat#10: bob@example.net [KEY bob@example.net] πŸ›‘οΈ +Single#Chat#1001: bob@example.net [KEY bob@example.net] -------------------------------------------------------------------------------- -Msg#10: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO πŸ›‘οΈ] -Msg#11πŸ”’: (Contact#Contact#10): Heyho from DC [FRESH] -Msg#13πŸ”’: Me (Contact#Contact#Self): Sending with DC again √ +Msg#1001: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO] +Msg#1002πŸ”’: (Contact#Contact#1001): Heyho from DC [FRESH] +Msg#1004πŸ”’: Me (Contact#Contact#Self): Sending with DC again √ -------------------------------------------------------------------------------- diff --git a/test-data/golden/two_group_securejoins b/test-data/golden/two_group_securejoins index 3684034fd7..5e20c36b6c 100644 --- a/test-data/golden/two_group_securejoins +++ b/test-data/golden/two_group_securejoins @@ -1,9 +1,9 @@ -Group#Chat#11: Group [3 member(s)] πŸ›‘οΈ +Group#Chat#6002: Group [3 member(s)] -------------------------------------------------------------------------------- -Msg#11: info (Contact#Contact#Info): alice@example.org invited you to join this group. +Msg#6004: info (Contact#Contact#Info): alice@example.org invited you to join this group. Waiting for the device of alice@example.org to reply… [NOTICED][INFO] -Msg#13: info (Contact#Contact#Info): alice@example.org replied, waiting for being added to the group… [NOTICED][INFO] -Msg#17: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO πŸ›‘οΈ] -Msg#18πŸ”’: (Contact#Contact#10): Member Me added by alice@example.org. [FRESH][INFO] +Msg#6006: info (Contact#Contact#Info): alice@example.org replied, waiting for being added to the group… [NOTICED][INFO] +Msg#6003: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO] +Msg#6008πŸ”’: (Contact#Contact#6001): Member Me added by alice@example.org. [FRESH][INFO] -------------------------------------------------------------------------------- diff --git a/test-data/golden/verified_chats_editor_reordering b/test-data/golden/verified_chats_editor_reordering index 21b949fb2b..3bf488c00c 100644 --- a/test-data/golden/verified_chats_editor_reordering +++ b/test-data/golden/verified_chats_editor_reordering @@ -1,11 +1,11 @@ -Group#Chat#11: Group [3 member(s)] πŸ›‘οΈ +Group#Chat#3002: Group [3 member(s)] -------------------------------------------------------------------------------- -Msg#11: info (Contact#Contact#Info): alice@example.org invited you to join this group. +Msg#3004: info (Contact#Contact#Info): alice@example.org invited you to join this group. Waiting for the device of alice@example.org to reply… [NOTICED][INFO] -Msg#13: info (Contact#Contact#Info): alice@example.org replied, waiting for being added to the group… [NOTICED][INFO] -Msg#16πŸ”’: (Contact#Contact#11): [FRESH] -Msg#18: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO πŸ›‘οΈ] -Msg#19: info (Contact#Contact#Info): Member bob@example.net added. [NOTICED][INFO] -Msg#20πŸ”’: (Contact#Contact#10): Member Me added by alice@example.org. [FRESH][INFO] +Msg#3006: info (Contact#Contact#Info): alice@example.org replied, waiting for being added to the group… [NOTICED][INFO] +Msg#3003: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO] +Msg#3008πŸ”’: (Contact#Contact#3002): [FRESH] +Msg#3009: info (Contact#Contact#Info): Member bob@example.net added. [NOTICED][INFO] +Msg#3010πŸ”’: (Contact#Contact#3001): Member Me added by alice@example.org. [FRESH][INFO] -------------------------------------------------------------------------------- diff --git a/test-data/image/1000x1000-bad-exif.jpg b/test-data/image/1000x1000-bad-exif.jpg new file mode 100644 index 0000000000..e39d6bf40d Binary files /dev/null and b/test-data/image/1000x1000-bad-exif.jpg differ diff --git a/test-data/message/verification-gossip-also-sent-to-from.eml b/test-data/message/verification-gossip-also-sent-to-from.eml index 27ee04a50c..f867fb7bf9 100644 --- a/test-data/message/verification-gossip-also-sent-to-from.eml +++ b/test-data/message/verification-gossip-also-sent-to-from.eml @@ -34,88 +34,112 @@ Content-Transfer-Encoding: 7bit -----BEGIN PGP MESSAGE----- -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 -=a+ak +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 +9itRo7h+e2Dv7NI9SbkWo+E= +=OL4B -----END PGP MESSAGE-----