mirror of
https://github.com/chatmail/core.git
synced 2026-09-21 12:39:12 +03:00
Compare commits
1 Commits
v1.124.0
...
link2xt/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22aa1b7b12 |
16
.github/workflows/ci.yml
vendored
16
.github/workflows/ci.yml
vendored
@@ -182,15 +182,15 @@ jobs:
|
||||
include:
|
||||
# Currently used Rust version.
|
||||
- os: ubuntu-latest
|
||||
python: 3.12
|
||||
python: 3.11
|
||||
- os: macos-latest
|
||||
python: 3.12
|
||||
python: 3.11
|
||||
|
||||
# PyPy tests
|
||||
- os: ubuntu-latest
|
||||
python: pypy3.10
|
||||
python: pypy3.9
|
||||
- os: macos-latest
|
||||
python: pypy3.10
|
||||
python: pypy3.9
|
||||
|
||||
# Minimum Supported Python Version = 3.7
|
||||
# This is the minimum version for which manylinux Python wheels are
|
||||
@@ -231,10 +231,6 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# Async Python bindings do not depend on Python version,
|
||||
# but are tested on Python 3.11 until Python 3.12 support
|
||||
# is added to `aiohttp` dependency:
|
||||
# https://github.com/aio-libs/aiohttp/issues/7646
|
||||
- os: ubuntu-latest
|
||||
python: 3.11
|
||||
- os: macos-latest
|
||||
@@ -242,9 +238,9 @@ jobs:
|
||||
|
||||
# PyPy tests
|
||||
- os: ubuntu-latest
|
||||
python: pypy3.10
|
||||
python: pypy3.9
|
||||
- os: macos-latest
|
||||
python: pypy3.10
|
||||
python: pypy3.9
|
||||
|
||||
# Minimum Supported Python Version = 3.8
|
||||
#
|
||||
|
||||
104
CHANGELOG.md
104
CHANGELOG.md
@@ -1,105 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [1.124.0] - 2023-10-04
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Return `DC_CONTACT_ID_SELF` from `dc_contact_get_verifier_id()` for directly verified contacts.
|
||||
- Deprecate `dc_contact_get_verifier_addr`.
|
||||
- python: use `dc_contact_get_verifier_id()`. `get_verifier()` returns a Contact rather than an address now.
|
||||
- Deprecate `get_next_media()`.
|
||||
- Ignore public key argument in `dc_preconfigure_keypair()`. Public key is extracted from the private key.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Wrap base64-encoded parts to 76 characters.
|
||||
- Require valid email addresses in `dc_provider_new_from_email[_with_dns]`.
|
||||
- Do not trash messages with attachments and no text when `location.kml` is attached ([#4749](https://github.com/deltachat/deltachat-core-rust/issues/4749)).
|
||||
- Initialise `last_msg_id` to the highest known row id. This ensures bots migrated from older version to `dc_get_next_msgs()` API do not process all previous messages from scratch.
|
||||
- Do not put the status footer into reaction MIME parts.
|
||||
- Ignore special chats in `get_similar_chat_ids()`. This prevents trash chat from showing up in similar chat list ([#4756](https://github.com/deltachat/deltachat-core-rust/issues/4756)).
|
||||
- Cap percentage in connectivity layout to 100% ([#4765](https://github.com/deltachat/deltachat-core-rust/pull/4765)).
|
||||
- Add Let's Encrypt root certificate to `reqwest`. This should allow scanning `DCACCOUNT` QR-codes on older Android phones when the server has a Let's Encrypt certificate.
|
||||
- deltachat-rpc-client: Increase stdio buffer to 64 MiB to avoid Python bots crashing when trying to load large messages via a JSON-RPC call.
|
||||
- Add `protected-headers` directive to Content-Type of encrypted messages with attachments ([#2302](https://github.com/deltachat/deltachat-core-rust/issues/2302)). This makes Thunderbird show encrypted Subject for Delta Chat messages.
|
||||
- webxdc: Reset `document.update` on forwarding. This fixes the test `test_forward_webxdc_instance()`.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Remove extra members from the local list in sake of group membership consistency ([#3782](https://github.com/deltachat/deltachat-core-rust/issues/3782)).
|
||||
- deltachat-rpc-client: Log exceptions when long-running tasks die.
|
||||
|
||||
### Build
|
||||
|
||||
- Build wheels for Python 3.12 and PyPy 3.10.
|
||||
|
||||
## [1.123.0] - 2023-09-22
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Make it possible to import secret key from a file with `DC_IMEX_IMPORT_SELF_KEYS`.
|
||||
- [**breaking**] Make `dc_jsonrpc_blocking_call` accept JSON-RPC request.
|
||||
|
||||
### Fixes
|
||||
|
||||
- `lookup_chat_by_reply()`: Skip not fully downloaded and undecipherable messages ([#4676](https://github.com/deltachat/deltachat-core-rust/pull/4676)).
|
||||
- `lookup_chat_by_reply()`: Skip undecipherable parent messages created by older versions ([#4676](https://github.com/deltachat/deltachat-core-rust/pull/4676)).
|
||||
- imex: Use "default" in the filename of the default key.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Update OpenSSL from 3.1.2 to 3.1.3.
|
||||
|
||||
## [1.122.0] - 2023-09-12
|
||||
|
||||
### API-Changes
|
||||
|
||||
- jsonrpc: Return only chat IDs for similar chats.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Reopen all connections on database passpharse change.
|
||||
- Do not block new group chats if 1:1 chat is blocked.
|
||||
- Improve group membership consistency algorithm ([#3782](https://github.com/deltachat/deltachat-core-rust/pull/3782))([#4624](https://github.com/deltachat/deltachat-core-rust/pull/4624)).
|
||||
- Forbid membership changes from possible non-members ([#3782](https://github.com/deltachat/deltachat-core-rust/pull/3782)).
|
||||
- `ChatId::parent_query()`: Don't filter out OutPending and OutFailed messages.
|
||||
|
||||
### Build system
|
||||
|
||||
- Update to OpenSSL 3.0.
|
||||
- Bump webpki from 0.22.0 to 0.22.1.
|
||||
- python: Add link to Mastodon into projects.urls.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add RSA-4096 key generation support.
|
||||
|
||||
### Refactor
|
||||
|
||||
- pgp: Add constants for encryption algorithm and hash.
|
||||
|
||||
## [1.121.0] - 2023-09-06
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add `dc_context_change_passphrase()`.
|
||||
- Add `Message.set_file_from_bytes()` API.
|
||||
- Add experimental API to get similar chats.
|
||||
|
||||
### Build system
|
||||
|
||||
- Build node packages on Ubuntu 18.04 instead of Debian 10.
|
||||
This reduces the requirement for glibc version from 2.28 to 2.27.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Allow membership changes by a MUA if we're not in the group ([#4624](https://github.com/deltachat/deltachat-core-rust/pull/4624)).
|
||||
- Save mime headers for messages not signed with a known key ([#4557](https://github.com/deltachat/deltachat-core-rust/pull/4557)).
|
||||
- Return from `dc_get_chatlist(DC_GCL_FOR_FORWARDING)` only chats where we can send ([#4616](https://github.com/deltachat/deltachat-core-rust/pull/4616)).
|
||||
- Do not allow dots at the end of email addresses.
|
||||
- deltachat-rpc-client: Remove `aiodns` optional dependency from required dependencies.
|
||||
`aiodns` depends on `pycares` which [fails to install in Termux](https://github.com/saghul/aiodns/issues/98).
|
||||
|
||||
## [1.120.0] - 2023-08-28
|
||||
|
||||
### API-Changes
|
||||
@@ -2864,7 +2764,3 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.119.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.118.0...v1.119.0
|
||||
[1.119.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.0...v1.119.1
|
||||
[1.120.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.1...v1.120.0
|
||||
[1.121.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.120.0...v1.121.0
|
||||
[1.122.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.121.0...v1.122.0
|
||||
[1.123.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.122.0...v1.123.0
|
||||
[1.124.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.123.0...v1.124.0
|
||||
|
||||
44
Cargo.lock
generated
44
Cargo.lock
generated
@@ -312,9 +312,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.6.20"
|
||||
version = "0.6.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
|
||||
checksum = "f8175979259124331c1d7bf6586ee7e0da434155e4b2d48ec2c8386281d8df39"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
@@ -1103,7 +1103,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.124.0"
|
||||
version = "1.120.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -1179,7 +1179,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.124.0"
|
||||
version = "1.120.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel",
|
||||
@@ -1203,7 +1203,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "1.124.0"
|
||||
version = "1.120.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -1218,7 +1218,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.124.0"
|
||||
version = "1.120.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1243,7 +1243,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.124.0"
|
||||
version = "1.120.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1605,7 +1605,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "email"
|
||||
version = "0.0.21"
|
||||
source = "git+https://github.com/deltachat/rust-email?branch=master#37778c89d5eb5a94b7983f3f37ff67769bde3cf9"
|
||||
source = "git+https://github.com/deltachat/rust-email?branch=master#25702df99254d059483b41417cd80696a258df8e"
|
||||
dependencies = [
|
||||
"base64 0.11.0",
|
||||
"chrono",
|
||||
@@ -3078,11 +3078,11 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.57"
|
||||
version = "0.10.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c"
|
||||
checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d"
|
||||
dependencies = [
|
||||
"bitflags 2.3.3",
|
||||
"bitflags 1.3.2",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
@@ -3110,18 +3110,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-src"
|
||||
version = "300.1.5+3.1.3"
|
||||
version = "111.26.0+1.1.1u"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "559068e4c12950d7dcaa1857a61725c0d38d4fc03ff8e070ab31a75d6e316491"
|
||||
checksum = "efc62c9f12b22b8f5208c23a7200a442b2e5999f8bdf80233852122b5a4f6f37"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.93"
|
||||
version = "0.9.90"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d"
|
||||
checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -4947,9 +4947,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.20.1"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c"
|
||||
checksum = "54319c93411147bced34cb5609a80e0a8e44c5999c93903a81cd866630ec0bfd"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
@@ -5159,13 +5159,13 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.20.1"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9"
|
||||
checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
@@ -5448,9 +5448,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "webpki"
|
||||
version = "0.22.2"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07ecc0cd7cac091bf682ec5efa18b1cff79d617b84181f38b3951dbe135f607f"
|
||||
checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.124.0"
|
||||
version = "1.120.0"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.65"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.124.0"
|
||||
version = "1.120.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -443,9 +443,7 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* DC_KEY_GEN_RSA2048 (1)=
|
||||
* generate RSA 2048 keypair
|
||||
* DC_KEY_GEN_ED25519 (2)=
|
||||
* generate Curve25519 keypair
|
||||
* DC_KEY_GEN_RSA4096 (3)=
|
||||
* generate RSA 4096 keypair
|
||||
* generate Ed25519 keypair
|
||||
* - `save_mime_headers` = 1=save mime headers
|
||||
* and make dc_get_mime_headers() work for subsequent calls,
|
||||
* 0=do not save mime headers (default)
|
||||
@@ -824,7 +822,7 @@ void dc_maybe_network (dc_context_t* context);
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param addr The e-mail address of the user. This must match the
|
||||
* configured_addr setting of the context as well as the UID of the key.
|
||||
* @param public_data Ignored, actual public key is extracted from secret_data.
|
||||
* @param public_data ASCII armored public key.
|
||||
* @param secret_data ASCII armored secret key.
|
||||
* @return 1 on success, 0 on failure.
|
||||
*/
|
||||
@@ -1481,7 +1479,6 @@ dc_array_t* dc_get_chat_media (dc_context_t* context, uint32_t ch
|
||||
* Typically used to implement the "next" and "previous" buttons
|
||||
* in a gallery or in a media player.
|
||||
*
|
||||
* @deprecated Deprecated 2023-10-03, use dc_get_chat_media() and navigate the returned array instead.
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param msg_id The ID of the current message from which the next or previous message should be searched.
|
||||
@@ -2285,7 +2282,6 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co
|
||||
*
|
||||
* - **DC_IMEX_IMPORT_SELF_KEYS** (2) - Import private keys found in the directory given as `param1`.
|
||||
* The last imported key is made the default keys unless its name contains the string `legacy`. Public keys are not imported.
|
||||
* If `param1` is a filename, import the private key from the file and make it the default.
|
||||
*
|
||||
* While dc_imex() returns immediately, the started job may take a while,
|
||||
* you can stop it using dc_stop_ongoing_process(). During execution of the job,
|
||||
@@ -5048,7 +5044,6 @@ int dc_contact_is_verified (dc_contact_t* contact);
|
||||
* A string containing the verifiers address. If it is the same address as the contact itself,
|
||||
* we verified the contact ourself. If it is an empty string, we don't have verifier
|
||||
* information or the contact is not verified.
|
||||
* @deprecated 2023-09-28, use dc_contact_get_verifier_id instead
|
||||
*/
|
||||
char* dc_contact_get_verifier_addr (dc_contact_t* contact);
|
||||
|
||||
@@ -5061,7 +5056,7 @@ char* dc_contact_get_verifier_addr (dc_contact_t* contact);
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
* @return
|
||||
* The contact ID of the verifier. If it is DC_CONTACT_ID_SELF,
|
||||
* The `ContactId` of the verifiers address. If it is the same address as the contact itself,
|
||||
* we verified the contact ourself. If it is 0, we don't have verifier information or
|
||||
* the contact is not verified.
|
||||
*/
|
||||
@@ -5757,11 +5752,12 @@ char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);
|
||||
*
|
||||
* @memberof dc_jsonrpc_instance_t
|
||||
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
|
||||
* @param input JSON-RPC request.
|
||||
* @param method JSON-RPC method name, e.g. `check_email_validity`.
|
||||
* @param params JSON-RPC method parameters, e.g. `["alice@example.org"]`.
|
||||
* @return JSON-RPC response as string, must be freed using dc_str_unref() after usage.
|
||||
* If there is no response, NULL is returned.
|
||||
* On error, NULL is returned.
|
||||
*/
|
||||
char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *input);
|
||||
char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *method, const char *params);
|
||||
|
||||
/**
|
||||
* @class dc_event_emitter_t
|
||||
@@ -6297,7 +6293,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_KEY_GEN_DEFAULT 0
|
||||
#define DC_KEY_GEN_RSA2048 1
|
||||
#define DC_KEY_GEN_ED25519 2
|
||||
#define DC_KEY_GEN_RSA4096 3
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,7 +29,7 @@ use deltachat::contact::{Contact, ContactId, Origin};
|
||||
use deltachat::context::Context;
|
||||
use deltachat::ephemeral::Timer as EphemeralTimer;
|
||||
use deltachat::imex::BackupProvider;
|
||||
use deltachat::key::{DcKey, DcSecretKey};
|
||||
use deltachat::key::DcKey;
|
||||
use deltachat::message::MsgId;
|
||||
use deltachat::net::read_url_blob;
|
||||
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
|
||||
@@ -805,7 +805,7 @@ pub unsafe extern "C" fn dc_maybe_network(context: *mut dc_context_t) {
|
||||
pub unsafe extern "C" fn dc_preconfigure_keypair(
|
||||
context: *mut dc_context_t,
|
||||
addr: *const libc::c_char,
|
||||
_public_data: *const libc::c_char,
|
||||
public_data: *const libc::c_char,
|
||||
secret_data: *const libc::c_char,
|
||||
) -> i32 {
|
||||
if context.is_null() {
|
||||
@@ -815,8 +815,8 @@ pub unsafe extern "C" fn dc_preconfigure_keypair(
|
||||
let ctx = &*context;
|
||||
block_on(async move {
|
||||
let addr = tools::EmailAddress::new(&to_string_lossy(addr))?;
|
||||
let public = key::SignedPublicKey::from_asc(&to_string_lossy(public_data))?.0;
|
||||
let secret = key::SignedSecretKey::from_asc(&to_string_lossy(secret_data))?.0;
|
||||
let public = secret.split_public_key()?;
|
||||
let keypair = key::KeyPair {
|
||||
addr,
|
||||
public,
|
||||
@@ -1431,7 +1431,6 @@ pub unsafe extern "C" fn dc_get_chat_media(
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[allow(deprecated)]
|
||||
pub unsafe extern "C" fn dc_get_next_media(
|
||||
context: *mut dc_context_t,
|
||||
msg_id: u32,
|
||||
@@ -2568,12 +2567,7 @@ pub unsafe extern "C" fn dc_set_location(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
location::set(ctx, latitude, longitude, accuracy)
|
||||
.await
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
}) as libc::c_int
|
||||
block_on(location::set(ctx, latitude, longitude, accuracy)) as _
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -4537,14 +4531,7 @@ 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 block_on(provider::get_provider_info(ctx, addr.as_str(), true)) {
|
||||
Some(provider) => provider,
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
@@ -4571,14 +4558,11 @@ pub unsafe extern "C" fn dc_provider_new_from_email_with_dns(
|
||||
|
||||
match socks5_enabled {
|
||||
Ok(socks5_enabled) => {
|
||||
match block_on(provider::get_provider_info_by_addr(
|
||||
match block_on(provider::get_provider_info(
|
||||
ctx,
|
||||
addr.as_str(),
|
||||
socks5_enabled,
|
||||
))
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
)) {
|
||||
Some(provider) => provider,
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
@@ -5026,7 +5010,7 @@ pub unsafe extern "C" fn dc_accounts_get_event_emitter(
|
||||
#[cfg(feature = "jsonrpc")]
|
||||
mod jsonrpc {
|
||||
use deltachat_jsonrpc::api::CommandApi;
|
||||
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
|
||||
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcServer, RpcSession};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -5102,24 +5086,25 @@ mod jsonrpc {
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_jsonrpc_blocking_call(
|
||||
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
|
||||
input: *const libc::c_char,
|
||||
method: *const libc::c_char,
|
||||
params: *const libc::c_char,
|
||||
) -> *mut libc::c_char {
|
||||
if jsonrpc_instance.is_null() {
|
||||
eprintln!("ignoring careless call to dc_jsonrpc_blocking_call()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let api = &*jsonrpc_instance;
|
||||
let input = to_string_lossy(input);
|
||||
let res = block_on(api.handle.process_incoming(&input));
|
||||
let method = to_string_lossy(method);
|
||||
let params = to_string_lossy(params);
|
||||
let params: Option<yerpc::Params> = match serde_json::from_str(¶ms) {
|
||||
Ok(params) => Some(params),
|
||||
Err(_) => None,
|
||||
};
|
||||
let params = params.map(yerpc::Params::into_value).unwrap_or_default();
|
||||
let res = block_on(api.handle.server().handle_request(method, params));
|
||||
match res {
|
||||
Some(message) => {
|
||||
if let Ok(message) = serde_json::to_string(&message) {
|
||||
message.strdup()
|
||||
} else {
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
None => ptr::null_mut(),
|
||||
Ok(res) => res.to_string().strdup(),
|
||||
Err(_) => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.124.0"
|
||||
version = "1.120.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
|
||||
@@ -39,6 +39,7 @@ pub mod types;
|
||||
use num_traits::FromPrimitive;
|
||||
use types::account::Account;
|
||||
use types::chat::FullChat;
|
||||
use types::chat_list::ChatListEntry;
|
||||
use types::contact::ContactObject;
|
||||
use types::events::Event;
|
||||
use types::http::HttpResponse;
|
||||
@@ -567,18 +568,22 @@ impl CommandApi {
|
||||
}
|
||||
|
||||
/// Returns chats similar to the given one.
|
||||
///
|
||||
/// Experimental API, subject to change without notice.
|
||||
async fn get_similar_chat_ids(&self, account_id: u32, chat_id: u32) -> Result<Vec<u32>> {
|
||||
async fn get_similar_chatlist_entries(
|
||||
&self,
|
||||
account_id: u32,
|
||||
chat_id: u32,
|
||||
) -> Result<Vec<ChatListEntry>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let chat_id = ChatId::new(chat_id);
|
||||
let list = chat_id
|
||||
.get_similar_chat_ids(&ctx)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|(chat_id, _metric)| chat_id.to_u32())
|
||||
.collect();
|
||||
Ok(list)
|
||||
let list = chat_id.get_similar_chatlist(&ctx).await?;
|
||||
let mut l: Vec<ChatListEntry> = Vec::with_capacity(list.len());
|
||||
for i in 0..list.len() {
|
||||
l.push(ChatListEntry(
|
||||
list.get_chat_id(i)?.to_u32(),
|
||||
list.get_msg_id(i)?.unwrap_or_default().to_u32(),
|
||||
));
|
||||
}
|
||||
Ok(l)
|
||||
}
|
||||
|
||||
async fn get_chatlist_items_by_entries(
|
||||
@@ -1427,10 +1432,6 @@ impl CommandApi {
|
||||
///
|
||||
/// one combined call for getting chat::get_next_media for both directions
|
||||
/// the manual chat::get_next_media in only one direction is not exposed by the jsonrpc yet
|
||||
///
|
||||
/// Deprecated 2023-10-03, use `get_chat_media` method
|
||||
/// and navigate the returned array instead.
|
||||
#[allow(deprecated)]
|
||||
async fn get_neighboring_chat_media(
|
||||
&self,
|
||||
account_id: u32,
|
||||
|
||||
@@ -8,12 +8,15 @@ use deltachat::{
|
||||
chatlist::Chatlist,
|
||||
};
|
||||
use num_traits::cast::ToPrimitive;
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
use super::color_int_to_hex_string;
|
||||
use super::message::MessageViewtype;
|
||||
|
||||
#[derive(Deserialize, Serialize, TypeDef, schemars::JsonSchema)]
|
||||
pub struct ChatListEntry(pub u32, pub u32);
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ChatListItemFetchResult {
|
||||
|
||||
@@ -318,7 +318,6 @@ pub enum DownloadState {
|
||||
Done,
|
||||
Available,
|
||||
Failure,
|
||||
Undecipherable,
|
||||
InProgress,
|
||||
}
|
||||
|
||||
@@ -328,7 +327,6 @@ impl From<download::DownloadState> for DownloadState {
|
||||
download::DownloadState::Done => DownloadState::Done,
|
||||
download::DownloadState::Available => DownloadState::Available,
|
||||
download::DownloadState::Failure => DownloadState::Failure,
|
||||
download::DownloadState::Undecipherable => DownloadState::Undecipherable,
|
||||
download::DownloadState::InProgress => DownloadState::InProgress,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,5 +55,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.124.0"
|
||||
"version": "1.120.0"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.124.0"
|
||||
version = "1.120.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
|
||||
|
||||
@@ -187,7 +187,6 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
DownloadState::Available => " [⬇ Download available]",
|
||||
DownloadState::InProgress => " [⬇ Download in progress...]️",
|
||||
DownloadState::Failure => " [⬇ Download failed]",
|
||||
DownloadState::Undecipherable => " [⬇ Decryption failed]",
|
||||
};
|
||||
|
||||
let temp2 = timestamp_to_str(msg.get_timestamp());
|
||||
@@ -894,7 +893,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
let latitude = arg1.parse()?;
|
||||
let longitude = arg2.parse()?;
|
||||
|
||||
let continue_streaming = location::set(&context, latitude, longitude, 0.).await?;
|
||||
let continue_streaming = location::set(&context, latitude, longitude, 0.).await;
|
||||
if continue_streaming {
|
||||
println!("Success, streaming should be continued.");
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
@@ -29,16 +28,10 @@ class Rpc:
|
||||
self.events_task: asyncio.Task
|
||||
|
||||
async def start(self) -> None:
|
||||
# Use buffer of 64 MiB.
|
||||
# Default limit as of Python 3.11 is 2**16 bytes, this is too low for some JSON-RPC responses,
|
||||
# such as loading large HTML message content.
|
||||
limit = 2**26
|
||||
|
||||
self.process = await asyncio.create_subprocess_exec(
|
||||
"deltachat-rpc-server",
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
limit=limit,
|
||||
**self._kwargs,
|
||||
)
|
||||
self.id = 0
|
||||
@@ -64,20 +57,16 @@ class Rpc:
|
||||
await self.close()
|
||||
|
||||
async def reader_loop(self) -> None:
|
||||
try:
|
||||
while True:
|
||||
line = await self.process.stdout.readline() # noqa
|
||||
if not line: # EOF
|
||||
break
|
||||
response = json.loads(line)
|
||||
if "id" in response:
|
||||
fut = self.request_events.pop(response["id"])
|
||||
fut.set_result(response)
|
||||
else:
|
||||
print(response)
|
||||
except Exception:
|
||||
# Log an exception if the reader loop dies.
|
||||
logging.exception("Exception in the reader loop")
|
||||
while True:
|
||||
line = await self.process.stdout.readline() # noqa
|
||||
if not line: # EOF
|
||||
break
|
||||
response = json.loads(line)
|
||||
if "id" in response:
|
||||
fut = self.request_events.pop(response["id"])
|
||||
fut.set_result(response)
|
||||
else:
|
||||
print(response)
|
||||
|
||||
async def get_queue(self, account_id: int) -> asyncio.Queue:
|
||||
if account_id not in self.event_queues:
|
||||
@@ -86,17 +75,13 @@ class Rpc:
|
||||
|
||||
async def events_loop(self) -> None:
|
||||
"""Requests new events and distributes them between queues."""
|
||||
try:
|
||||
while True:
|
||||
if self.closing:
|
||||
return
|
||||
event = await self.get_next_event()
|
||||
account_id = event["contextId"]
|
||||
queue = await self.get_queue(account_id)
|
||||
await queue.put(event["event"])
|
||||
except Exception:
|
||||
# Log an exception if the event loop dies.
|
||||
logging.exception("Exception in the event loop")
|
||||
while True:
|
||||
if self.closing:
|
||||
return
|
||||
event = await self.get_next_event()
|
||||
account_id = event["contextId"]
|
||||
queue = await self.get_queue(account_id)
|
||||
await queue.put(event["event"])
|
||||
|
||||
async def wait_for_event(self, account_id: int) -> Optional[dict]:
|
||||
"""Waits for the next event from the given account and returns it."""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.124.0"
|
||||
version = "1.120.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -3,6 +3,11 @@ unmaintained = "allow"
|
||||
ignore = [
|
||||
"RUSTSEC-2020-0071",
|
||||
"RUSTSEC-2022-0093",
|
||||
|
||||
# Exponential CPU time usage for TLS certificate processing in webpki.
|
||||
# It is only used for backup transfer, so does not affect IMAP and SMTP connections.
|
||||
# Waiting for `iroh` to update dependencies.
|
||||
"RUSTSEC-2023-0052",
|
||||
]
|
||||
|
||||
[bans]
|
||||
|
||||
@@ -90,7 +90,6 @@ module.exports = {
|
||||
DC_KEY_GEN_DEFAULT: 0,
|
||||
DC_KEY_GEN_ED25519: 2,
|
||||
DC_KEY_GEN_RSA2048: 1,
|
||||
DC_KEY_GEN_RSA4096: 3,
|
||||
DC_LP_AUTH_NORMAL: 4,
|
||||
DC_LP_AUTH_OAUTH2: 2,
|
||||
DC_MEDIA_QUALITY_BALANCED: 0,
|
||||
|
||||
@@ -90,7 +90,6 @@ export enum C {
|
||||
DC_KEY_GEN_DEFAULT = 0,
|
||||
DC_KEY_GEN_ED25519 = 2,
|
||||
DC_KEY_GEN_RSA2048 = 1,
|
||||
DC_KEY_GEN_RSA4096 = 3,
|
||||
DC_LP_AUTH_NORMAL = 4,
|
||||
DC_LP_AUTH_OAUTH2 = 2,
|
||||
DC_MEDIA_QUALITY_BALANCED = 0,
|
||||
|
||||
@@ -60,5 +60,5 @@
|
||||
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.124.0"
|
||||
"version": "1.120.0"
|
||||
}
|
||||
|
||||
@@ -24,5 +24,3 @@ ignore_missing_imports = True
|
||||
[mypy-imap_tools.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-distutils.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
@@ -34,7 +34,6 @@ dynamic = [
|
||||
"Home" = "https://github.com/deltachat/deltachat-core-rust/"
|
||||
"Bug Tracker" = "https://github.com/deltachat/deltachat-core-rust/issues"
|
||||
"Documentation" = "https://py.delta.chat/"
|
||||
"Mastodon" = "https://chaos.social/@delta"
|
||||
|
||||
[project.entry-points.pytest11]
|
||||
"deltachat.testplugin" = "deltachat.testplugin"
|
||||
|
||||
@@ -195,7 +195,7 @@ class Account:
|
||||
assert res != ffi.NULL, f"config value not found for: {name!r}"
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
def _preconfigure_keypair(self, addr: str, secret: str) -> None:
|
||||
def _preconfigure_keypair(self, addr: str, public: str, secret: str) -> None:
|
||||
"""See dc_preconfigure_keypair() in deltachat.h.
|
||||
|
||||
In other words, you don't need this.
|
||||
@@ -203,7 +203,7 @@ class Account:
|
||||
res = lib.dc_preconfigure_keypair(
|
||||
self._dc_context,
|
||||
as_dc_charpointer(addr),
|
||||
ffi.NULL,
|
||||
as_dc_charpointer(public),
|
||||
as_dc_charpointer(secret),
|
||||
)
|
||||
if res == 0:
|
||||
|
||||
@@ -75,12 +75,9 @@ class Contact:
|
||||
"""Return True if the contact is verified."""
|
||||
return lib.dc_contact_is_verified(self._dc_contact) == 2
|
||||
|
||||
def get_verifier(self, contact) -> Optional["Contact"]:
|
||||
def get_verifier(self, contact):
|
||||
"""Return the address of the contact that verified the contact."""
|
||||
verifier_id = lib.dc_contact_get_verifier_id(contact._dc_contact)
|
||||
if verifier_id == 0:
|
||||
return None
|
||||
return Contact(self.account, verifier_id)
|
||||
return from_dc_charpointer(lib.dc_contact_get_verifier_addr(contact._dc_contact))
|
||||
|
||||
def get_profile_image(self) -> Optional[str]:
|
||||
"""Get contact profile image.
|
||||
|
||||
@@ -478,9 +478,10 @@ class ACFactory:
|
||||
except IndexError:
|
||||
pass
|
||||
else:
|
||||
fname_pub = self.data.read_path(f"key/{keyname}-public.asc")
|
||||
fname_sec = self.data.read_path(f"key/{keyname}-secret.asc")
|
||||
if fname_sec:
|
||||
account._preconfigure_keypair(addr, fname_sec)
|
||||
if fname_pub and fname_sec:
|
||||
account._preconfigure_keypair(addr, fname_pub, fname_sec)
|
||||
return True
|
||||
print(f"WARN: could not use preconfigured keys for {addr!r}")
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import deltachat as dc
|
||||
|
||||
|
||||
class TestGroupStressTests:
|
||||
@@ -150,8 +149,9 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
assert msg.is_encrypted()
|
||||
|
||||
lp.sec("ac2: Check that ac2 verified ac1")
|
||||
# If we verified the contact ourselves then verifier addr == contact addr
|
||||
ac2_ac1_contact = ac2.get_contacts()[0]
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact).id == dc.const.DC_CONTACT_ID_SELF
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact) == ac1_addr
|
||||
|
||||
lp.sec("ac2: send message and let ac1 read it")
|
||||
chat2.send_text("world")
|
||||
@@ -176,9 +176,9 @@ 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
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact) == ac1_addr
|
||||
ac2_ac3_contact = ac2.get_contacts()[1]
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac3_contact).addr == ac1_addr
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac3_contact) == ac1_addr
|
||||
|
||||
lp.sec("ac2: send message and let ac3 read it")
|
||||
chat2.send_text("hi")
|
||||
|
||||
@@ -1679,36 +1679,6 @@ def test_qr_join_chat(acfactory, lp):
|
||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
|
||||
|
||||
def test_qr_new_group_unblocked(acfactory, lp):
|
||||
"""Regression test for a bug intoduced in core v1.113.0.
|
||||
ac2 scans a verified group QR code created by ac1.
|
||||
This results in creation of a blocked 1:1 chat with ac1 on ac2,
|
||||
but ac1 contact is not blocked on ac2.
|
||||
Then ac1 creates a group, adds ac2 there and promotes it by sending a message.
|
||||
ac2 should receive a message and create a contact request for the group.
|
||||
Due to a bug previously ac2 created a blocked group.
|
||||
"""
|
||||
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1_chat = ac1.create_group_chat("Group for joining", verified=True)
|
||||
qr = ac1_chat.get_join_qr()
|
||||
ac2.qr_join_chat(qr)
|
||||
|
||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
|
||||
ac1_new_chat = ac1.create_group_chat("Another group")
|
||||
ac1_new_chat.add_contact(ac2)
|
||||
ac1_new_chat.send_text("Hello!")
|
||||
|
||||
# Receive "Member added" message.
|
||||
ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
# Receive "Hello!" message.
|
||||
ac2_msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert ac2_msg.text == "Hello!"
|
||||
assert ac2_msg.chat.is_contact_request()
|
||||
|
||||
|
||||
def test_qr_email_capitalization(acfactory, lp):
|
||||
"""Regression test for a bug
|
||||
that resulted in failure to propagate verification via gossip in a verified group
|
||||
|
||||
@@ -67,9 +67,10 @@ class TestOfflineAccountBasic:
|
||||
|
||||
def test_preconfigure_keypair(self, acfactory, data):
|
||||
ac = acfactory.get_unconfigured_account()
|
||||
alice_public = data.read_path("key/alice-public.asc")
|
||||
alice_secret = data.read_path("key/alice-secret.asc")
|
||||
assert alice_secret
|
||||
ac._preconfigure_keypair("alice@example.org", alice_secret)
|
||||
assert alice_public and alice_secret
|
||||
ac._preconfigure_keypair("alice@example.org", alice_public, alice_secret)
|
||||
|
||||
def test_getinfo(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import json
|
||||
from queue import Queue
|
||||
|
||||
import deltachat as dc
|
||||
@@ -227,26 +226,10 @@ def test_jsonrpc_blocking_call(tmp_path):
|
||||
lib.dc_accounts_unref,
|
||||
)
|
||||
jsonrpc = ffi.gc(lib.dc_jsonrpc_init(accounts), lib.dc_jsonrpc_unref)
|
||||
res = json.loads(
|
||||
from_optional_dc_charpointer(
|
||||
lib.dc_jsonrpc_blocking_call(
|
||||
jsonrpc,
|
||||
json.dumps(
|
||||
{"jsonrpc": "2.0", "method": "check_email_validity", "params": ["alice@example.org"], "id": "123"},
|
||||
).encode("utf-8"),
|
||||
),
|
||||
),
|
||||
res = from_optional_dc_charpointer(
|
||||
lib.dc_jsonrpc_blocking_call(jsonrpc, b"check_email_validity", b'["alice@example.org"]'),
|
||||
)
|
||||
assert res == {"jsonrpc": "2.0", "id": "123", "result": True}
|
||||
assert res == "true"
|
||||
|
||||
res = json.loads(
|
||||
from_optional_dc_charpointer(
|
||||
lib.dc_jsonrpc_blocking_call(
|
||||
jsonrpc,
|
||||
json.dumps(
|
||||
{"jsonrpc": "2.0", "method": "check_email_validity", "params": ["alice"], "id": "456"},
|
||||
).encode("utf-8"),
|
||||
),
|
||||
),
|
||||
)
|
||||
assert res == {"jsonrpc": "2.0", "id": "456", "result": False}
|
||||
res = from_optional_dc_charpointer(lib.dc_jsonrpc_blocking_call(jsonrpc, b"check_email_validity", b'["alice"]'))
|
||||
assert res == "false"
|
||||
|
||||
@@ -1 +1 @@
|
||||
2023-10-04
|
||||
2023-08-28
|
||||
@@ -31,7 +31,7 @@ unset DCC_NEW_TMP_EMAIL
|
||||
|
||||
# Try to build wheels for a range of interpreters, but don't fail if they are not available.
|
||||
# E.g. musllinux_1_1 does not have PyPy interpreters as of 2022-07-10
|
||||
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,py312,pypy37,pypy38,pypy39,pypy310 --skip-missing-interpreters true
|
||||
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,pypy37,pypy38,pypy39 --skip-missing-interpreters true
|
||||
|
||||
auditwheel repair "$TOXWORKDIR"/wheelhouse/deltachat* -w "$TOXWORKDIR/wheelhouse"
|
||||
|
||||
|
||||
64
src/chat.rs
64
src/chat.rs
@@ -23,7 +23,6 @@ use crate::constants::{
|
||||
use crate::contact::{Contact, ContactId, Origin, VerifiedStatus};
|
||||
use crate::context::Context;
|
||||
use crate::debug_logging::maybe_set_logging_xdc;
|
||||
use crate::download::DownloadState;
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::events::EventType;
|
||||
use crate::html::new_html_mimepart;
|
||||
@@ -918,10 +917,9 @@ impl ChatId {
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT chat_id, count(*) AS n
|
||||
FROM chats_contacts
|
||||
WHERE contact_id > ? AND chat_id > ?
|
||||
FROM chats_contacts where contact_id > 9
|
||||
GROUP BY chat_id",
|
||||
(ContactId::LAST_SPECIAL, DC_CHAT_ID_LAST_SPECIAL),
|
||||
(),
|
||||
|row| {
|
||||
let chat_id: ChatId = row.get(0)?;
|
||||
let size: f64 = row.get(1)?;
|
||||
@@ -1040,14 +1038,11 @@ impl ChatId {
|
||||
T: Send + 'static,
|
||||
{
|
||||
let sql = &context.sql;
|
||||
// Do not reply to not fully downloaded messages. Such a message could be a group chat
|
||||
// message that we assigned to 1:1 chat.
|
||||
let query = format!(
|
||||
"SELECT {fields} \
|
||||
FROM msgs WHERE chat_id=? AND state NOT IN (?, ?) AND NOT hidden AND download_state={} \
|
||||
FROM msgs WHERE chat_id=? AND state NOT IN (?, ?, ?, ?) AND NOT hidden \
|
||||
ORDER BY timestamp DESC, id DESC \
|
||||
LIMIT 1;",
|
||||
DownloadState::Done as u32,
|
||||
LIMIT 1;"
|
||||
);
|
||||
let row = sql
|
||||
.query_row_optional(
|
||||
@@ -1056,11 +1051,8 @@ impl ChatId {
|
||||
self,
|
||||
MessageState::OutPreparing,
|
||||
MessageState::OutDraft,
|
||||
// We don't filter `OutPending` and `OutFailed` messages because the new message
|
||||
// for which `parent_query()` is done may assume that it will be received in a
|
||||
// context affected by those messages, e.g. they could add new members to a
|
||||
// group and the new message will contain them in "To:". Anyway recipients must
|
||||
// be prepared to orphaned references.
|
||||
MessageState::OutPending,
|
||||
MessageState::OutFailed,
|
||||
),
|
||||
f,
|
||||
)
|
||||
@@ -1072,17 +1064,34 @@ impl ChatId {
|
||||
self,
|
||||
context: &Context,
|
||||
) -> Result<Option<(String, String, String)>> {
|
||||
self.parent_query(
|
||||
context,
|
||||
"rfc724_mid, mime_in_reply_to, mime_references",
|
||||
|row: &rusqlite::Row| {
|
||||
let rfc724_mid: String = row.get(0)?;
|
||||
let mime_in_reply_to: String = row.get(1)?;
|
||||
let mime_references: String = row.get(2)?;
|
||||
Ok((rfc724_mid, mime_in_reply_to, mime_references))
|
||||
},
|
||||
)
|
||||
.await
|
||||
if let Some((rfc724_mid, mime_in_reply_to, mime_references, error)) = self
|
||||
.parent_query(
|
||||
context,
|
||||
"rfc724_mid, mime_in_reply_to, mime_references, error",
|
||||
|row: &rusqlite::Row| {
|
||||
let rfc724_mid: String = row.get(0)?;
|
||||
let mime_in_reply_to: String = row.get(1)?;
|
||||
let mime_references: String = row.get(2)?;
|
||||
let error: String = row.get(3)?;
|
||||
Ok((rfc724_mid, mime_in_reply_to, mime_references, error))
|
||||
},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
if !error.is_empty() {
|
||||
// Do not reply to error messages.
|
||||
//
|
||||
// An error message could be a group chat message that we failed to decrypt and
|
||||
// assigned to 1:1 chat. A reply to it will show up as a reply to group message
|
||||
// on the other side. To avoid such situations, it is better not to reply to
|
||||
// error messages at all.
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some((rfc724_mid, mime_in_reply_to, mime_references)))
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns multi-line text summary of encryption preferences of all chat contacts.
|
||||
@@ -2898,9 +2907,6 @@ pub enum Direction {
|
||||
}
|
||||
|
||||
/// Searches next/previous message based on the given message and list of types.
|
||||
///
|
||||
/// Deprecated since 2023-10-03.
|
||||
#[deprecated(note = "use `get_chat_media` instead")]
|
||||
pub async fn get_next_media(
|
||||
context: &Context,
|
||||
curr_msg_id: MsgId,
|
||||
@@ -3532,8 +3538,6 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
msg.param.remove(Param::ForcePlaintext);
|
||||
msg.param.remove(Param::Cmd);
|
||||
msg.param.remove(Param::OverrideSenderDisplayname);
|
||||
msg.param.remove(Param::WebxdcDocument);
|
||||
msg.param.remove(Param::WebxdcDocumentTimestamp);
|
||||
msg.param.remove(Param::WebxdcSummary);
|
||||
msg.param.remove(Param::WebxdcSummaryTimestamp);
|
||||
msg.in_reply_to = None;
|
||||
|
||||
@@ -62,15 +62,8 @@ pub enum MediaQuality {
|
||||
pub enum KeyGenType {
|
||||
#[default]
|
||||
Default = 0,
|
||||
|
||||
/// 2048-bit RSA.
|
||||
Rsa2048 = 1,
|
||||
|
||||
/// [Ed25519](https://ed25519.cr.yp.to/) signature and X25519 encryption.
|
||||
Ed25519 = 2,
|
||||
|
||||
/// 4096-bit RSA.
|
||||
Rsa4096 = 3,
|
||||
}
|
||||
|
||||
/// Video chat URL type.
|
||||
@@ -238,7 +231,6 @@ mod tests {
|
||||
assert_eq!(KeyGenType::Default, KeyGenType::from_i32(0).unwrap());
|
||||
assert_eq!(KeyGenType::Rsa2048, KeyGenType::from_i32(1).unwrap());
|
||||
assert_eq!(KeyGenType::Ed25519, KeyGenType::from_i32(2).unwrap());
|
||||
assert_eq!(KeyGenType::Rsa4096, KeyGenType::from_i32(3).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -109,7 +109,7 @@ impl ContactId {
|
||||
|
||||
/// ID of the contact for device messages.
|
||||
pub const DEVICE: ContactId = ContactId::new(5);
|
||||
pub(crate) const LAST_SPECIAL: ContactId = ContactId::new(9);
|
||||
const LAST_SPECIAL: ContactId = ContactId::new(9);
|
||||
|
||||
/// Address to go with [`ContactId::DEVICE`].
|
||||
///
|
||||
@@ -1252,22 +1252,11 @@ impl Contact {
|
||||
|
||||
/// Returns the ContactId that verified the contact.
|
||||
pub async fn get_verifier_id(&self, context: &Context) -> Result<Option<ContactId>> {
|
||||
let Some(verifier_addr) = self.get_verifier_addr(context).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if verifier_addr == self.addr {
|
||||
// Contact is directly verified via QR code.
|
||||
return Ok(Some(ContactId::SELF));
|
||||
}
|
||||
|
||||
match Contact::lookup_id_by_addr(context, &verifier_addr, Origin::AddressBook).await? {
|
||||
Some(contact_id) => Ok(Some(contact_id)),
|
||||
None => {
|
||||
let addr = &self.addr;
|
||||
warn!(context, "Could not lookup contact with address {verifier_addr} which introduced {addr}.");
|
||||
Ok(None)
|
||||
}
|
||||
let verifier_addr = self.get_verifier_addr(context).await?;
|
||||
if let Some(addr) = verifier_addr {
|
||||
Ok(Contact::lookup_id_by_addr(context, &addr, Origin::AddressBook).await?)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -382,7 +382,7 @@ impl Context {
|
||||
translated_stockstrings: stockstrings,
|
||||
events,
|
||||
scheduler: SchedulerState::new(),
|
||||
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow at least 1 message every 10 seconds + a burst of 6.
|
||||
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow to send 6 messages immediately, no more than once every 10 seconds.
|
||||
quota: RwLock::new(None),
|
||||
quota_update_request: AtomicBool::new(false),
|
||||
resync_request: AtomicBool::new(false),
|
||||
@@ -814,22 +814,7 @@ impl Context {
|
||||
pub async fn get_next_msgs(&self) -> Result<Vec<MsgId>> {
|
||||
let last_msg_id = match self.get_config(Config::LastMsgId).await? {
|
||||
Some(s) => MsgId::new(s.parse()?),
|
||||
None => {
|
||||
// If `last_msg_id` is not set yet,
|
||||
// subtract 1 from the last id,
|
||||
// so a single message is returned and can
|
||||
// be marked as seen.
|
||||
self.sql
|
||||
.query_row(
|
||||
"SELECT IFNULL((SELECT MAX(id) - 1 FROM msgs), 0)",
|
||||
(),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
Ok(msg_id)
|
||||
},
|
||||
)
|
||||
.await?
|
||||
}
|
||||
None => MsgId::new_unset(),
|
||||
};
|
||||
|
||||
let list = self
|
||||
@@ -1474,35 +1459,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_context_change_passphrase() -> Result<()> {
|
||||
let dir = tempdir()?;
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
|
||||
let id = 1;
|
||||
let context = Context::new_closed(&dbfile, id, Events::new(), StockStrings::new())
|
||||
.await
|
||||
.context("failed to create context")?;
|
||||
assert_eq!(context.open("foo".to_string()).await?, true);
|
||||
assert_eq!(context.is_open().await, true);
|
||||
|
||||
context
|
||||
.set_config(Config::Addr, Some("alice@example.org"))
|
||||
.await?;
|
||||
|
||||
context
|
||||
.change_passphrase("bar".to_string())
|
||||
.await
|
||||
.context("Failed to change passphrase")?;
|
||||
|
||||
assert_eq!(
|
||||
context.get_config(Config::Addr).await?.unwrap(),
|
||||
"alice@example.org"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ongoing() -> Result<()> {
|
||||
let context = TestContext::new().await;
|
||||
|
||||
@@ -59,9 +59,6 @@ pub enum DownloadState {
|
||||
/// Failed to fully download the message.
|
||||
Failure = 20,
|
||||
|
||||
/// Undecipherable message.
|
||||
Undecipherable = 30,
|
||||
|
||||
/// Full download of the message is in progress.
|
||||
InProgress = 1000,
|
||||
}
|
||||
@@ -83,9 +80,7 @@ impl MsgId {
|
||||
pub async fn download_full(self, context: &Context) -> Result<()> {
|
||||
let msg = Message::load_from_db(context, self).await?;
|
||||
match msg.download_state() {
|
||||
DownloadState::Done | DownloadState::Undecipherable => {
|
||||
return Err(anyhow!("Nothing to download."))
|
||||
}
|
||||
DownloadState::Done => return Err(anyhow!("Nothing to download.")),
|
||||
DownloadState::InProgress => return Err(anyhow!("Download already in progress.")),
|
||||
DownloadState::Available | DownloadState::Failure => {
|
||||
self.update_download_state(context, DownloadState::InProgress)
|
||||
|
||||
@@ -77,6 +77,9 @@ pub enum HeaderDef {
|
||||
SecureJoinAuth,
|
||||
Sender,
|
||||
|
||||
/// [`Supersedes`](https://www.rfc-editor.org/rfc/rfc4021.html#section-2.1.46) header.
|
||||
Supersedes,
|
||||
|
||||
/// Ephemeral message timer.
|
||||
EphemeralTimer,
|
||||
Received,
|
||||
|
||||
119
src/imex.rs
119
src/imex.rs
@@ -586,74 +586,63 @@ async fn export_backup_inner(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Imports secret key from a file.
|
||||
async fn import_secret_key(context: &Context, path: &Path, set_default: bool) -> Result<()> {
|
||||
let buf = read_file(context, &path).await?;
|
||||
let armored = std::string::String::from_utf8_lossy(&buf);
|
||||
set_self_key(context, &armored, set_default, false).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Imports secret keys from the provided file or directory.
|
||||
///
|
||||
/// If provided path is a file, ASCII-armored secret key is read from the file
|
||||
/// and set as the default key.
|
||||
///
|
||||
/// If provided path is a directory, all files with .asc extension
|
||||
/// containing secret keys are imported and the last successfully
|
||||
/// imported which does not contain "legacy" in its filename
|
||||
/// is set as the default.
|
||||
async fn import_self_keys(context: &Context, path: &Path) -> Result<()> {
|
||||
let attr = tokio::fs::metadata(path).await?;
|
||||
|
||||
if attr.is_file() {
|
||||
info!(
|
||||
context,
|
||||
"Importing secret key from {} as the default key.",
|
||||
path.display()
|
||||
);
|
||||
let set_default = true;
|
||||
import_secret_key(context, path, set_default).await?;
|
||||
return Ok(());
|
||||
}
|
||||
/*******************************************************************************
|
||||
* Classic key import
|
||||
******************************************************************************/
|
||||
async fn import_self_keys(context: &Context, dir: &Path) -> Result<()> {
|
||||
/* hint: even if we switch to import Autocrypt Setup Files, we should leave the possibility to import
|
||||
plain ASC keys, at least keys without a password, if we do not want to implement a password entry function.
|
||||
Importing ASC keys is useful to use keys in Delta Chat used by any other non-Autocrypt-PGP implementation.
|
||||
|
||||
Maybe we should make the "default" key handlong also a little bit smarter
|
||||
(currently, the last imported key is the standard key unless it contains the string "legacy" in its name) */
|
||||
let mut set_default: bool;
|
||||
let mut imported_cnt = 0;
|
||||
|
||||
let mut dir_handle = tokio::fs::read_dir(&path).await?;
|
||||
let dir_name = dir.to_string_lossy();
|
||||
let mut dir_handle = tokio::fs::read_dir(&dir).await?;
|
||||
while let Ok(Some(entry)) = dir_handle.next_entry().await {
|
||||
let entry_fn = entry.file_name();
|
||||
let name_f = entry_fn.to_string_lossy();
|
||||
let path_plus_name = path.join(&entry_fn);
|
||||
if let Some(suffix) = get_filesuffix_lc(&name_f) {
|
||||
if suffix != "asc" {
|
||||
let path_plus_name = dir.join(&entry_fn);
|
||||
match get_filesuffix_lc(&name_f) {
|
||||
Some(suffix) => {
|
||||
if suffix != "asc" {
|
||||
continue;
|
||||
}
|
||||
set_default = if name_f.contains("legacy") {
|
||||
info!(context, "found legacy key '{}'", path_plus_name.display());
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
None => {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
let set_default = !name_f.contains("legacy");
|
||||
}
|
||||
info!(
|
||||
context,
|
||||
"Considering key file: {}.",
|
||||
"considering key file: {}",
|
||||
path_plus_name.display()
|
||||
);
|
||||
|
||||
if let Err(err) = import_secret_key(context, &path_plus_name, set_default).await {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to import secret key from {}: {:#}.",
|
||||
path_plus_name.display(),
|
||||
err
|
||||
);
|
||||
continue;
|
||||
match read_file(context, &path_plus_name).await {
|
||||
Ok(buf) => {
|
||||
let armored = std::string::String::from_utf8_lossy(&buf);
|
||||
if let Err(err) = set_self_key(context, &armored, set_default, false).await {
|
||||
info!(context, "set_self_key: {}", err);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Err(_) => continue,
|
||||
}
|
||||
|
||||
imported_cnt += 1;
|
||||
}
|
||||
ensure!(
|
||||
imported_cnt > 0,
|
||||
"No private keys found in {}.",
|
||||
path.display()
|
||||
"No private keys found in \"{}\".",
|
||||
dir_name
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -684,8 +673,7 @@ async fn export_self_keys(context: &Context, dir: &Path) -> Result<()> {
|
||||
.await?;
|
||||
|
||||
for (id, public_key, private_key, is_default) in keys {
|
||||
let id = Some(id).filter(|_| is_default == 0);
|
||||
|
||||
let id = Some(id).filter(|_| is_default != 0);
|
||||
if let Ok(key) = public_key {
|
||||
if let Err(err) = export_key_to_asc_file(context, dir, id, &key).await {
|
||||
error!(context, "Failed to export public key: {:#}.", err);
|
||||
@@ -876,35 +864,14 @@ mod tests {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_export_and_import_key() {
|
||||
let export_dir = tempfile::tempdir().unwrap();
|
||||
|
||||
let context = TestContext::new_alice().await;
|
||||
if let Err(err) = imex(
|
||||
&context.ctx,
|
||||
ImexMode::ExportSelfKeys,
|
||||
export_dir.path(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
let blobdir = context.ctx.get_blobdir();
|
||||
if let Err(err) = imex(&context.ctx, ImexMode::ExportSelfKeys, blobdir, None).await {
|
||||
panic!("got error on export: {err:#}");
|
||||
}
|
||||
|
||||
let context2 = TestContext::new_alice().await;
|
||||
if let Err(err) = imex(
|
||||
&context2.ctx,
|
||||
ImexMode::ImportSelfKeys,
|
||||
export_dir.path(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
panic!("got error on import: {err:#}");
|
||||
}
|
||||
|
||||
let keyfile = export_dir.path().join("private-key-default.asc");
|
||||
let context3 = TestContext::new_alice().await;
|
||||
if let Err(err) = imex(&context3.ctx, ImexMode::ImportSelfKeys, &keyfile, None).await {
|
||||
if let Err(err) = imex(&context2.ctx, ImexMode::ImportSelfKeys, blobdir, None).await {
|
||||
panic!("got error on import: {err:#}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,13 +328,13 @@ pub async fn is_sending_locations_to_chat(
|
||||
}
|
||||
|
||||
/// Sets current location of the user device.
|
||||
pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> Result<bool> {
|
||||
pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> bool {
|
||||
if latitude == 0.0 && longitude == 0.0 {
|
||||
return Ok(true);
|
||||
return true;
|
||||
}
|
||||
let mut continue_streaming = false;
|
||||
|
||||
let chats = context
|
||||
if let Ok(chats) = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id FROM chats WHERE locations_send_until>?;",
|
||||
@@ -346,29 +346,33 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
for chat_id in chats {
|
||||
context.sql.execute(
|
||||
"INSERT INTO locations \
|
||||
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
|
||||
(
|
||||
latitude,
|
||||
longitude,
|
||||
accuracy,
|
||||
time(),
|
||||
chat_id,
|
||||
ContactId::SELF,
|
||||
)).await.context("Failed to store location")?;
|
||||
|
||||
info!(context, "Stored location for chat {chat_id}.");
|
||||
continue_streaming = true;
|
||||
.await
|
||||
{
|
||||
for chat_id in chats {
|
||||
if let Err(err) = context.sql.execute(
|
||||
"INSERT INTO locations \
|
||||
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
|
||||
(
|
||||
latitude,
|
||||
longitude,
|
||||
accuracy,
|
||||
time(),
|
||||
chat_id,
|
||||
ContactId::SELF,
|
||||
)
|
||||
).await {
|
||||
warn!(context, "failed to store location {:#}", err);
|
||||
} else {
|
||||
info!(context, "stored location for chat {}", chat_id);
|
||||
continue_streaming = true;
|
||||
}
|
||||
}
|
||||
if continue_streaming {
|
||||
context.emit_event(EventType::LocationChanged(Some(ContactId::SELF)));
|
||||
};
|
||||
}
|
||||
if continue_streaming {
|
||||
context.emit_event(EventType::LocationChanged(Some(ContactId::SELF)));
|
||||
};
|
||||
|
||||
Ok(continue_streaming)
|
||||
continue_streaming
|
||||
}
|
||||
|
||||
/// Searches for locations in the given time range, optionally filtering by chat and contact IDs.
|
||||
@@ -460,7 +464,7 @@ pub async fn delete_all(context: &Context) -> Result<()> {
|
||||
}
|
||||
|
||||
/// Returns `location.kml` contents.
|
||||
pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<Option<(String, u32)>> {
|
||||
pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)> {
|
||||
let mut last_added_location_id = 0;
|
||||
|
||||
let self_addr = context.get_primary_self_addr().await?;
|
||||
@@ -530,11 +534,9 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<Option<(Strin
|
||||
ret += "</Document>\n</kml>";
|
||||
}
|
||||
|
||||
if location_count > 0 {
|
||||
Ok(Some((ret, last_added_location_id)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
ensure!(location_count > 0, "No locations processed");
|
||||
|
||||
Ok((ret, last_added_location_id))
|
||||
}
|
||||
|
||||
fn get_kml_timestamp(utc: i64) -> String {
|
||||
@@ -926,38 +928,4 @@ Content-Disposition: attachment; filename="location.kml"
|
||||
assert_eq!(locations.len(), 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_locations_to_chat() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
send_locations_to_chat(&alice, alice_chat.id, 1000).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let msg = bob.recv_msg(&sent).await;
|
||||
assert_eq!(msg.text, "Location streaming enabled by alice@example.org.");
|
||||
let bob_chat_id = msg.chat_id;
|
||||
|
||||
assert_eq!(set(&alice, 10.0, 20.0, 1.0).await?, true);
|
||||
|
||||
// Send image without text.
|
||||
let file_name = "image.png";
|
||||
let bytes = include_bytes!("../test-data/image/logo.png");
|
||||
let file = alice.get_blobdir().join(file_name);
|
||||
tokio::fs::write(&file, bytes).await?;
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
let sent = alice.send_msg(alice_chat.id, &mut msg).await;
|
||||
|
||||
let msg = bob.recv_msg_opt(&sent).await.unwrap();
|
||||
assert!(msg.chat_id == bob_chat_id);
|
||||
assert_eq!(msg.msg_ids.len(), 1);
|
||||
|
||||
let bob_msg = Message::load_from_db(&bob, *msg.msg_ids.get(0).unwrap()).await?;
|
||||
assert_eq!(bob_msg.chat_id, bob_chat_id);
|
||||
assert_eq!(bob_msg.viewtype, Viewtype::Image);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,6 +435,18 @@ pub struct Message {
|
||||
|
||||
/// `In-Reply-To` header value.
|
||||
pub(crate) in_reply_to: Option<String>,
|
||||
|
||||
/// `Supersedes` header value.
|
||||
///
|
||||
/// It is only used for sending and not stored in the database.
|
||||
///
|
||||
/// The header contains `Message-ID` of the original message
|
||||
/// superseded by this one.
|
||||
///
|
||||
/// The header is specified
|
||||
/// in <https://www.rfc-editor.org/rfc/rfc4021.html#section-2.1.46>.
|
||||
pub(crate) supersedes: Option<String>,
|
||||
|
||||
pub(crate) is_dc_message: MessengerMessage,
|
||||
pub(crate) mime_modified: bool,
|
||||
pub(crate) chat_blocked: Blocked,
|
||||
@@ -530,6 +542,7 @@ impl Message {
|
||||
download_state: row.get("download_state")?,
|
||||
error: Some(row.get::<_, String>("error")?)
|
||||
.filter(|error| !error.is_empty()),
|
||||
supersedes: None, // `Supersedes` header is only used for sending and not stored in the database.
|
||||
is_dc_message: row.get("msgrmsg")?,
|
||||
mime_modified: row.get("mime_modified")?,
|
||||
text,
|
||||
|
||||
@@ -549,6 +549,11 @@ impl<'a> MimeFactory<'a> {
|
||||
Loaded::Mdn { .. } => create_outgoing_rfc724_mid(None, &self.from_addr),
|
||||
};
|
||||
let rfc724_mid_headervalue = render_rfc724_mid(&rfc724_mid);
|
||||
if let Some(supersedes) = &self.msg.supersedes {
|
||||
headers
|
||||
.protected
|
||||
.push(Header::new("Supersedes".into(), supersedes.to_string()));
|
||||
}
|
||||
|
||||
// Amazon's SMTP servers change the `Message-ID`, just as Outlook's SMTP servers do.
|
||||
// Outlook's servers add an `X-Microsoft-Original-Message-ID` header with the original `Message-ID`,
|
||||
@@ -678,12 +683,6 @@ impl<'a> MimeFactory<'a> {
|
||||
})
|
||||
};
|
||||
|
||||
let get_content_type_directives_header = || {
|
||||
(
|
||||
"Content-Type-Deltachat-Directives".to_string(),
|
||||
"protected-headers=\"v1\"".to_string(),
|
||||
)
|
||||
};
|
||||
let outer_message = if is_encrypted {
|
||||
headers.protected.push(from_header);
|
||||
|
||||
@@ -720,7 +719,10 @@ impl<'a> MimeFactory<'a> {
|
||||
if !existing_ct.ends_with(';') {
|
||||
existing_ct += ";";
|
||||
}
|
||||
let message = message.header(get_content_type_directives_header());
|
||||
let message = message.replace_header(Header::new(
|
||||
"Content-Type".to_string(),
|
||||
format!("{existing_ct} protected-headers=\"v1\";"),
|
||||
));
|
||||
|
||||
// Set the appropriate Content-Type for the outer message
|
||||
let outer_message = PartBuilder::new().header((
|
||||
@@ -789,12 +791,11 @@ impl<'a> MimeFactory<'a> {
|
||||
{
|
||||
message
|
||||
} else {
|
||||
let message = message.header(get_content_type_directives_header());
|
||||
let (payload, signature) = encrypt_helper.sign(context, message).await?;
|
||||
PartBuilder::new()
|
||||
.header((
|
||||
"Content-Type",
|
||||
"multipart/signed; protocol=\"application/pgp-signature\"",
|
||||
"Content-Type".to_string(),
|
||||
"multipart/signed; protocol=\"application/pgp-signature\"".to_string(),
|
||||
))
|
||||
.child(payload)
|
||||
.child(
|
||||
@@ -864,13 +865,9 @@ impl<'a> MimeFactory<'a> {
|
||||
}
|
||||
|
||||
/// Returns MIME part with a `location.kml` attachment.
|
||||
async fn get_location_kml_part(&mut self, context: &Context) -> Result<Option<PartBuilder>> {
|
||||
let Some((kml_content, last_added_location_id)) =
|
||||
location::get_kml(context, self.msg.chat_id).await?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
async fn get_location_kml_part(&mut self, context: &Context) -> Result<PartBuilder> {
|
||||
let (kml_content, last_added_location_id) =
|
||||
location::get_kml(context, self.msg.chat_id).await?;
|
||||
let part = PartBuilder::new()
|
||||
.content_type(
|
||||
&"application/vnd.google-earth.kml+xml"
|
||||
@@ -886,7 +883,7 @@ impl<'a> MimeFactory<'a> {
|
||||
// otherwise, the independent location is already filed
|
||||
self.last_added_location_id = last_added_location_id;
|
||||
}
|
||||
Ok(Some(part))
|
||||
Ok(part)
|
||||
}
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
@@ -1176,10 +1173,7 @@ impl<'a> MimeFactory<'a> {
|
||||
}
|
||||
let flowed_text = format_flowed(final_text);
|
||||
|
||||
let is_reaction = self.msg.param.get_int(Param::Reaction).unwrap_or_default() != 0;
|
||||
|
||||
let footer = if is_reaction { "" } else { &self.selfstatus };
|
||||
|
||||
let footer = &self.selfstatus;
|
||||
let message_text = format!(
|
||||
"{}{}{}{}{}{}",
|
||||
fwdhint.unwrap_or_default(),
|
||||
@@ -1202,7 +1196,7 @@ impl<'a> MimeFactory<'a> {
|
||||
))
|
||||
.body(message_text);
|
||||
|
||||
if is_reaction {
|
||||
if self.msg.param.get_int(Param::Reaction).unwrap_or_default() != 0 {
|
||||
main_part = main_part.header(("Content-Disposition", "reaction"));
|
||||
}
|
||||
|
||||
@@ -1241,8 +1235,11 @@ impl<'a> MimeFactory<'a> {
|
||||
}
|
||||
|
||||
if location::is_sending_locations_to_chat(context, Some(self.msg.chat_id)).await? {
|
||||
if let Some(part) = self.get_location_kml_part(context).await? {
|
||||
parts.push(part);
|
||||
match self.get_location_kml_part(context).await {
|
||||
Ok(part) => parts.push(part),
|
||||
Err(err) => {
|
||||
warn!(context, "mimefactory: could not send location: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1256,7 +1253,7 @@ impl<'a> MimeFactory<'a> {
|
||||
} else if command == SystemMessage::WebxdcStatusUpdate {
|
||||
let json = self.msg.param.get(Param::Arg).unwrap_or_default();
|
||||
parts.push(context.build_status_update_part(json));
|
||||
} else if self.msg.viewtype == Viewtype::Webxdc {
|
||||
} else if self.msg.viewtype == Viewtype::Webxdc && self.msg.supersedes.is_none() {
|
||||
if let Some(json) = context
|
||||
.render_webxdc_status_update_object(self.msg.id, None)
|
||||
.await?
|
||||
@@ -1371,16 +1368,15 @@ impl<'a> MimeFactory<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns base64-encoded buffer `buf` split into 76-bytes long
|
||||
/// Returns base64-encoded buffer `buf` split into 78-bytes long
|
||||
/// chunks separated by CRLF.
|
||||
///
|
||||
/// [RFC2045 specification of base64 Content-Transfer-Encoding](https://datatracker.ietf.org/doc/html/rfc2045#section-6.8)
|
||||
/// says that "The encoded output stream must be represented in lines of no more than 76 characters each."
|
||||
/// Longer lines trigger `BASE64_LENGTH_78_79` rule of SpamAssassin.
|
||||
/// This line length limit is an
|
||||
/// [RFC5322 requirement](https://tools.ietf.org/html/rfc5322#section-2.1.1).
|
||||
pub(crate) fn wrapped_base64_encode(buf: &[u8]) -> String {
|
||||
let base64 = base64::engine::general_purpose::STANDARD.encode(buf);
|
||||
let mut chars = base64.chars();
|
||||
std::iter::repeat_with(|| chars.by_ref().take(76).collect::<String>())
|
||||
std::iter::repeat_with(|| chars.by_ref().take(78).collect::<String>())
|
||||
.take_while(|s| !s.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\r\n")
|
||||
@@ -1539,7 +1535,6 @@ fn maybe_encode_words(words: &str) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use mailparse::{addrparse_header, MailHeaderMap};
|
||||
use std::str;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::ChatId;
|
||||
@@ -1548,11 +1543,10 @@ mod tests {
|
||||
ProtectionStatus,
|
||||
};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants;
|
||||
use crate::contact::{ContactAddress, Origin};
|
||||
use crate::mimeparser::MimeMessage;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{get_chat_msg, TestContext, TestContextManager};
|
||||
use crate::test_utils::{get_chat_msg, TestContext};
|
||||
#[test]
|
||||
fn test_render_email_address() {
|
||||
let display_name = "ä space";
|
||||
@@ -1622,8 +1616,8 @@ mod tests {
|
||||
fn test_wrapped_base64_encode() {
|
||||
let input = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
let output =
|
||||
"QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB\r\n\
|
||||
QUFBQUFBQUFBQQ==";
|
||||
"QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU\r\n\
|
||||
FBQUFBQUFBQQ==";
|
||||
assert_eq!(wrapped_base64_encode(input), output);
|
||||
}
|
||||
|
||||
@@ -2201,11 +2195,7 @@ mod tests {
|
||||
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
|
||||
|
||||
let part = payload.next().unwrap();
|
||||
assert_eq!(
|
||||
part.match_indices("multipart/mixed; protected-headers=\"v1\"")
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert_eq!(part.match_indices("multipart/mixed").count(), 1);
|
||||
assert_eq!(part.match_indices("Subject:").count(), 1);
|
||||
assert_eq!(part.match_indices("Autocrypt:").count(), 0);
|
||||
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
|
||||
@@ -2317,37 +2307,4 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_protected_headers_directive() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let chat = tcm
|
||||
.send_recv_accept(&alice, &bob, "alice->bob")
|
||||
.await
|
||||
.chat_id;
|
||||
|
||||
// Now Bob can send an encrypted message to Alice.
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
// Long messages are truncated and MimeMessage::decoded_data is set for them. We need
|
||||
// decoded_data to check presense of the necessary headers.
|
||||
msg.set_text("a".repeat(constants::DC_DESIRED_TEXT_LEN + 1));
|
||||
msg.set_file_from_bytes(&bob, "foo.bar", "content".as_bytes(), None)
|
||||
.await?;
|
||||
let sent = bob.send_msg(chat, &mut msg).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
let mime = MimeMessage::from_bytes(&alice, sent.payload.as_bytes(), None).await?;
|
||||
let mut payload = str::from_utf8(&mime.decoded_data)?.splitn(2, "\r\n\r\n");
|
||||
let part = payload.next().unwrap();
|
||||
assert_eq!(
|
||||
part.match_indices("multipart/mixed; protected-headers=\"v1\"")
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert_eq!(part.match_indices("Subject:").count(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -438,8 +438,6 @@ impl MimeMessage {
|
||||
typ: Viewtype::Text,
|
||||
msg_raw: Some(txt.clone()),
|
||||
msg: txt,
|
||||
// Don't change the error prefix for now,
|
||||
// receive_imf.rs:lookup_chat_by_reply() checks it.
|
||||
error: Some(format!("Decrypting failed: {err:#}")),
|
||||
..Default::default()
|
||||
};
|
||||
@@ -804,6 +802,18 @@ impl MimeMessage {
|
||||
|
||||
// Boxed future to deal with recursion
|
||||
async move {
|
||||
if mail.ctype.params.get("protected-headers").is_some() {
|
||||
if mail.ctype.mimetype == "text/rfc822-headers" {
|
||||
warn!(
|
||||
context,
|
||||
"Protected headers found in text/rfc822-headers attachment: Will be ignored.",
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
warn!(context, "Ignoring nested protected headers");
|
||||
}
|
||||
|
||||
enum MimeS {
|
||||
Multiple,
|
||||
Single,
|
||||
@@ -840,10 +850,7 @@ impl MimeMessage {
|
||||
|
||||
self.parse_mime_recursive(context, &mail, is_related).await
|
||||
}
|
||||
MimeS::Single => {
|
||||
self.add_single_part_if_known(context, mail, is_related)
|
||||
.await
|
||||
}
|
||||
MimeS::Single => self.add_single_part_if_known(context, mail, is_related).await,
|
||||
}
|
||||
}
|
||||
.boxed()
|
||||
@@ -1433,36 +1440,33 @@ impl MimeMessage {
|
||||
let (report_fields, _) = mailparse::parse_headers(&report_body)?;
|
||||
|
||||
// must be present
|
||||
if report_fields
|
||||
.get_header_value(HeaderDef::Disposition)
|
||||
.is_none()
|
||||
{
|
||||
warn!(
|
||||
context,
|
||||
"Ignoring unknown disposition-notification, Message-Id: {:?}.",
|
||||
report_fields.get_header_value(HeaderDef::MessageId)
|
||||
);
|
||||
return Ok(None);
|
||||
};
|
||||
if let Some(_disposition) = report_fields.get_header_value(HeaderDef::Disposition) {
|
||||
let original_message_id = report_fields
|
||||
.get_header_value(HeaderDef::OriginalMessageId)
|
||||
// MS Exchange doesn't add an Original-Message-Id header. Instead, they put
|
||||
// the original message id into the In-Reply-To header:
|
||||
.or_else(|| report.headers.get_header_value(HeaderDef::InReplyTo))
|
||||
.and_then(|v| parse_message_id(&v).ok());
|
||||
let additional_message_ids = report_fields
|
||||
.get_header_value(HeaderDef::AdditionalMessageIds)
|
||||
.map_or_else(Vec::new, |v| {
|
||||
v.split(' ')
|
||||
.filter_map(|s| parse_message_id(s).ok())
|
||||
.collect()
|
||||
});
|
||||
|
||||
let original_message_id = report_fields
|
||||
.get_header_value(HeaderDef::OriginalMessageId)
|
||||
// MS Exchange doesn't add an Original-Message-Id header. Instead, they put
|
||||
// the original message id into the In-Reply-To header:
|
||||
.or_else(|| report.headers.get_header_value(HeaderDef::InReplyTo))
|
||||
.and_then(|v| parse_message_id(&v).ok());
|
||||
let additional_message_ids = report_fields
|
||||
.get_header_value(HeaderDef::AdditionalMessageIds)
|
||||
.map_or_else(Vec::new, |v| {
|
||||
v.split(' ')
|
||||
.filter_map(|s| parse_message_id(s).ok())
|
||||
.collect()
|
||||
});
|
||||
return Ok(Some(Report {
|
||||
original_message_id,
|
||||
additional_message_ids,
|
||||
}));
|
||||
}
|
||||
warn!(
|
||||
context,
|
||||
"ignoring unknown disposition-notification, Message-Id: {:?}",
|
||||
report_fields.get_header_value(HeaderDef::MessageId)
|
||||
);
|
||||
|
||||
Ok(Some(Report {
|
||||
original_message_id,
|
||||
additional_message_ids,
|
||||
}))
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn process_delivery_status(
|
||||
|
||||
@@ -4,20 +4,12 @@ use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use mime::Mime;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::socks::Socks5Config;
|
||||
|
||||
const HTTP_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
static LETSENCRYPT_ROOT: Lazy<reqwest::tls::Certificate> = Lazy::new(|| {
|
||||
reqwest::tls::Certificate::from_der(include_bytes!(
|
||||
"../../assets/root-certificates/letsencrypt/isrgrootx1.der"
|
||||
))
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
/// HTTP(S) GET response.
|
||||
#[derive(Debug)]
|
||||
pub struct Response {
|
||||
@@ -87,10 +79,7 @@ async fn read_url_inner(context: &Context, url: &str) -> Result<reqwest::Respons
|
||||
}
|
||||
|
||||
pub(crate) fn get_client(socks5_config: Option<Socks5Config>) -> Result<reqwest::Client> {
|
||||
let builder = reqwest::ClientBuilder::new()
|
||||
.timeout(HTTP_TIMEOUT)
|
||||
.add_root_certificate(LETSENCRYPT_ROOT.clone());
|
||||
|
||||
let builder = reqwest::ClientBuilder::new().timeout(HTTP_TIMEOUT);
|
||||
let builder = if let Some(socks5_config) = socks5_config {
|
||||
let proxy = reqwest::Proxy::all(socks5_config.to_url())?;
|
||||
builder.proxy(proxy)
|
||||
|
||||
19
src/pgp.rs
19
src/pgp.rs
@@ -30,12 +30,6 @@ pub(crate) const HEADER_AUTOCRYPT: &str = "autocrypt-prefer-encrypt";
|
||||
#[allow(missing_docs)]
|
||||
pub const HEADER_SETUPCODE: &str = "passphrase-begin";
|
||||
|
||||
/// Preferred symmetric encryption algorithm.
|
||||
const SYMMETRIC_KEY_ALGORITHM: SymmetricKeyAlgorithm = SymmetricKeyAlgorithm::AES128;
|
||||
|
||||
/// Preferred cryptographic hash.
|
||||
const HASH_ALGORITHM: HashAlgorithm = HashAlgorithm::SHA2_256;
|
||||
|
||||
/// A wrapper for rPGP public key types
|
||||
#[derive(Debug)]
|
||||
enum SignedPublicKeyOrSubkey<'a> {
|
||||
@@ -142,7 +136,6 @@ pub struct KeyPair {
|
||||
pub(crate) fn create_keypair(addr: EmailAddress, keygen_type: KeyGenType) -> Result<KeyPair> {
|
||||
let (secret_key_type, public_key_type) = match keygen_type {
|
||||
KeyGenType::Rsa2048 => (PgpKeyType::Rsa(2048), PgpKeyType::Rsa(2048)),
|
||||
KeyGenType::Rsa4096 => (PgpKeyType::Rsa(4096), PgpKeyType::Rsa(4096)),
|
||||
KeyGenType::Ed25519 | KeyGenType::Default => (PgpKeyType::EdDSA, PgpKeyType::ECDH),
|
||||
};
|
||||
|
||||
@@ -256,13 +249,11 @@ pub async fn pk_encrypt(
|
||||
// TODO: measure time
|
||||
let encrypted_msg = if let Some(ref skey) = private_key_for_signing {
|
||||
lit_msg
|
||||
.sign(skey, || "".into(), HASH_ALGORITHM)
|
||||
.sign(skey, || "".into(), Default::default())
|
||||
.and_then(|msg| msg.compress(CompressionAlgorithm::ZLIB))
|
||||
.and_then(|msg| {
|
||||
msg.encrypt_to_keys(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs)
|
||||
})
|
||||
.and_then(|msg| msg.encrypt_to_keys(&mut rng, Default::default(), &pkeys_refs))
|
||||
} else {
|
||||
lit_msg.encrypt_to_keys(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs)
|
||||
lit_msg.encrypt_to_keys(&mut rng, Default::default(), &pkeys_refs)
|
||||
};
|
||||
|
||||
let msg = encrypted_msg?;
|
||||
@@ -281,7 +272,7 @@ pub fn pk_calc_signature(
|
||||
let msg = Message::new_literal_bytes("", plain).sign(
|
||||
private_key_for_signing,
|
||||
|| "".into(),
|
||||
HASH_ALGORITHM,
|
||||
Default::default(),
|
||||
)?;
|
||||
let signature = msg.into_signature().to_armored_string(None)?;
|
||||
Ok(signature)
|
||||
@@ -378,7 +369,7 @@ pub async fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result<String> {
|
||||
let mut rng = thread_rng();
|
||||
let s2k = StringToKey::new_default(&mut rng);
|
||||
let msg =
|
||||
lit_msg.encrypt_with_password(&mut rng, s2k, SYMMETRIC_KEY_ALGORITHM, || passphrase)?;
|
||||
lit_msg.encrypt_with_password(&mut rng, s2k, Default::default(), || passphrase)?;
|
||||
|
||||
let encoded_msg = msg.to_armored_string(None)?;
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ use trust_dns_resolver::{config, AsyncResolver, TokioAsyncResolver};
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::provider::data::{PROVIDER_DATA, PROVIDER_IDS};
|
||||
use crate::tools::EmailAddress;
|
||||
|
||||
/// Provider status according to manual testing.
|
||||
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
||||
@@ -176,30 +175,21 @@ fn get_resolver() -> Result<TokioAsyncResolver> {
|
||||
Ok(resolver)
|
||||
}
|
||||
|
||||
/// 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<Option<&'static Provider>> {
|
||||
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.
|
||||
///
|
||||
/// For compatibility, email address can be passed to this function
|
||||
/// instead of the domain.
|
||||
pub async fn get_provider_info(
|
||||
context: &Context,
|
||||
domain: &str,
|
||||
skip_mx: bool,
|
||||
) -> Option<&'static Provider> {
|
||||
let domain = domain.rsplit('@').next()?;
|
||||
|
||||
if let Some(provider) = get_provider_by_domain(domain) {
|
||||
return Some(provider);
|
||||
}
|
||||
@@ -324,25 +314,15 @@ mod tests {
|
||||
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());
|
||||
// get_provider_info() accepts email addresses for backwards compatibility
|
||||
assert!(
|
||||
get_provider_info_by_addr(&t, "example@google.com", false)
|
||||
.await?
|
||||
get_provider_info(&t, "example@google.com", false)
|
||||
.await
|
||||
.unwrap()
|
||||
.id
|
||||
== "gmail"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -464,16 +464,6 @@ Content-Disposition: reaction\n\
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
// Test that the status does not get mixed up into reactions.
|
||||
alice
|
||||
.set_config(
|
||||
Config::Selfstatus,
|
||||
Some("Buy Delta Chat today and make this banner go away!"),
|
||||
)
|
||||
.await?;
|
||||
bob.set_config(Config::Selfstatus, Some("Sent from my Delta Chat Pro. 👍"))
|
||||
.await?;
|
||||
|
||||
let chat_alice = alice.create_chat(&bob).await;
|
||||
let alice_msg = alice.send_text(chat_alice.id, "Hi!").await;
|
||||
let bob_msg = bob.recv_msg(&alice_msg).await;
|
||||
|
||||
@@ -62,6 +62,10 @@ pub struct ReceivedMsg {
|
||||
|
||||
/// Whether IMAP messages should be immediately deleted.
|
||||
pub needs_delete_job: bool,
|
||||
|
||||
/// Message-ID saved into the database.
|
||||
/// For messages with `Supersedes` header this is the Message-ID of the original message.
|
||||
pub rfc724_mid: String,
|
||||
}
|
||||
|
||||
/// Emulates reception of a message from the network.
|
||||
@@ -113,26 +117,28 @@ pub(crate) async fn receive_imf_inner(
|
||||
{
|
||||
Err(err) => {
|
||||
warn!(context, "receive_imf: can't parse MIME: {err:#}.");
|
||||
if rfc724_mid.starts_with(GENERATED_PREFIX) {
|
||||
// We don't have an rfc724_mid, there's no point in adding a trash entry
|
||||
let msg_ids;
|
||||
if !rfc724_mid.starts_with(GENERATED_PREFIX) {
|
||||
let row_id = context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO msgs(rfc724_mid, chat_id) VALUES (?,?)",
|
||||
(rfc724_mid, DC_CHAT_ID_TRASH),
|
||||
)
|
||||
.await?;
|
||||
msg_ids = vec![MsgId::new(u32::try_from(row_id)?)];
|
||||
} else {
|
||||
return Ok(None);
|
||||
// We don't have an rfc724_mid, there's no point in adding a trash entry
|
||||
}
|
||||
|
||||
let row_id = context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO msgs(rfc724_mid, chat_id) VALUES (?,?)",
|
||||
(rfc724_mid, DC_CHAT_ID_TRASH),
|
||||
)
|
||||
.await?;
|
||||
let msg_ids = vec![MsgId::new(u32::try_from(row_id)?)];
|
||||
|
||||
return Ok(Some(ReceivedMsg {
|
||||
chat_id: DC_CHAT_ID_TRASH,
|
||||
state: MessageState::Undefined,
|
||||
sort_timestamp: 0,
|
||||
msg_ids,
|
||||
needs_delete_job: false,
|
||||
rfc724_mid: rfc724_mid.to_string(),
|
||||
}));
|
||||
}
|
||||
Ok(mime_parser) => mime_parser,
|
||||
@@ -337,12 +343,12 @@ pub(crate) async fn receive_imf_inner(
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE imap SET target=? WHERE rfc724_mid=?",
|
||||
(target, rfc724_mid),
|
||||
(target, &received_msg.rfc724_mid),
|
||||
)
|
||||
.await?;
|
||||
} else if !mime_parser.mdn_reports.is_empty() && mime_parser.has_chat_version() {
|
||||
// This is a Delta Chat MDN. Mark as read.
|
||||
markseen_on_imap_table(context, rfc724_mid).await?;
|
||||
markseen_on_imap_table(context, &received_msg.rfc724_mid).await?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -545,30 +551,19 @@ async fn add_parts(
|
||||
// signals whether the current user is a bot
|
||||
let is_bot = context.get_config_bool(Config::Bot).await?;
|
||||
|
||||
let create_blocked_default = if is_bot {
|
||||
Blocked::Not
|
||||
} else {
|
||||
Blocked::Request
|
||||
};
|
||||
let create_blocked = if let Some(ChatIdBlocked { id: _, blocked }) = test_normal_chat {
|
||||
match blocked {
|
||||
Blocked::Request => create_blocked_default,
|
||||
Blocked::Not => Blocked::Not,
|
||||
Blocked::Yes => {
|
||||
if Contact::is_blocked_load(context, from_id).await? {
|
||||
// User has blocked the contact.
|
||||
// Block the group contact created as well.
|
||||
Blocked::Yes
|
||||
} else {
|
||||
// 1:1 chat is blocked, but the contact is not.
|
||||
// This happens when 1:1 chat is hidden
|
||||
// during scanning of a group invitation code.
|
||||
Blocked::Request
|
||||
}
|
||||
let create_blocked = match test_normal_chat {
|
||||
Some(ChatIdBlocked {
|
||||
id: _,
|
||||
blocked: Blocked::Request,
|
||||
}) if is_bot => Blocked::Not,
|
||||
Some(ChatIdBlocked { id: _, blocked }) => blocked,
|
||||
None => {
|
||||
if is_bot {
|
||||
Blocked::Not
|
||||
} else {
|
||||
Blocked::Request
|
||||
}
|
||||
}
|
||||
} else {
|
||||
create_blocked_default
|
||||
};
|
||||
|
||||
if chat_id.is_none() {
|
||||
@@ -1060,6 +1055,43 @@ async fn add_parts(
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
let rfc724_mid = if let Some(supersedes) = mime_parser.get_header(HeaderDef::Supersedes) {
|
||||
supersedes.to_string()
|
||||
} else {
|
||||
rfc724_mid.to_string()
|
||||
};
|
||||
|
||||
let supersedes_msg_id = match mime_parser.get_header(HeaderDef::Supersedes) {
|
||||
Some(supersedes) => {
|
||||
if let Some(msg_id) = rfc724_mid_exists(context, supersedes).await? {
|
||||
if let Some(orig_from_id) = context
|
||||
.sql
|
||||
.query_row_optional("SELECT from_id FROM msgs WHERE id=?", (msg_id,), |row| {
|
||||
let from_id: ContactId = row.get(0)?;
|
||||
|
||||
Ok(from_id)
|
||||
})
|
||||
.await?
|
||||
{
|
||||
if from_id == orig_from_id {
|
||||
Some(msg_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
if supersedes_msg_id.is_some() {
|
||||
replace_msg_id = supersedes_msg_id;
|
||||
info!(context, "Superseding {supersedes_msg_id:?}");
|
||||
}
|
||||
|
||||
// fine, so far. now, split the message into simple parts usable as "short messages"
|
||||
// and add them to the database (mails sent by other messenger clients should result
|
||||
// into only one message; mails sent by other clients may result in several messages
|
||||
@@ -1128,8 +1160,7 @@ async fn add_parts(
|
||||
(&part.msg, part.typ)
|
||||
};
|
||||
|
||||
let part_is_empty =
|
||||
typ == Viewtype::Text && msg.is_empty() && part.param.get(Param::Quote).is_none();
|
||||
let part_is_empty = part.msg.is_empty() && part.param.get(Param::Quote).is_none();
|
||||
let mime_modified = save_mime_modified && !part_is_empty;
|
||||
if mime_modified {
|
||||
// Avoid setting mime_modified for more than one part.
|
||||
@@ -1154,8 +1185,7 @@ async fn add_parts(
|
||||
|
||||
// If you change which information is skipped if the message is trashed,
|
||||
// also change `MsgId::trash()` and `delete_expired_messages()`
|
||||
let trash =
|
||||
chat_id.is_trash() || (is_location_kml && msg.is_empty() && typ == Viewtype::Text);
|
||||
let trash = chat_id.is_trash() || (is_location_kml && msg.is_empty());
|
||||
|
||||
let row_id = context
|
||||
.sql
|
||||
@@ -1228,8 +1258,6 @@ RETURNING id
|
||||
ephemeral_timestamp,
|
||||
if is_partial_download.is_some() {
|
||||
DownloadState::Available
|
||||
} else if mime_parser.decrypting_failed {
|
||||
DownloadState::Undecipherable
|
||||
} else {
|
||||
DownloadState::Done
|
||||
},
|
||||
@@ -1315,6 +1343,7 @@ RETURNING id
|
||||
sort_timestamp,
|
||||
msg_ids: created_db_entries,
|
||||
needs_delete_job,
|
||||
rfc724_mid,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1409,53 +1438,49 @@ async fn lookup_chat_by_reply(
|
||||
) -> Result<Option<(ChatId, Blocked)>> {
|
||||
// Try to assign message to the same chat as the parent message.
|
||||
|
||||
let Some(parent) = parent else {
|
||||
return Ok(None);
|
||||
};
|
||||
if let Some(parent) = parent {
|
||||
let parent_chat = Chat::load_from_db(context, parent.chat_id).await?;
|
||||
|
||||
let parent_chat = Chat::load_from_db(context, parent.chat_id).await?;
|
||||
|
||||
if parent.download_state != DownloadState::Done
|
||||
// TODO (2023-09-12): Added for backward compatibility with versions that did not have
|
||||
// `DownloadState::Undecipherable`. Remove eventually with the comment in
|
||||
// `MimeMessage::from_bytes()`.
|
||||
|| parent
|
||||
.error
|
||||
.as_ref()
|
||||
.filter(|e| e.starts_with("Decrypting failed:"))
|
||||
.is_some()
|
||||
{
|
||||
// If the parent msg is not fully downloaded or undecipherable, it may have been
|
||||
// assigned to the wrong chat (they often get assigned to the 1:1 chat with the sender).
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if parent_chat.id == DC_CHAT_ID_TRASH {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// If this was a private message just to self, it was probably a private reply.
|
||||
// It should not go into the group then, but into the private chat.
|
||||
if is_probably_private_reply(context, to_ids, from_id, mime_parser, parent_chat.id).await? {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// If the parent chat is a 1:1 chat, and the sender is a classical MUA and added
|
||||
// a new person to TO/CC, then the message should not go to the 1:1 chat, but to a
|
||||
// newly created ad-hoc group.
|
||||
if parent_chat.typ == Chattype::Single && !mime_parser.has_chat_version() && to_ids.len() > 1 {
|
||||
let mut chat_contacts = chat::get_chat_contacts(context, parent_chat.id).await?;
|
||||
chat_contacts.push(ContactId::SELF);
|
||||
if to_ids.iter().any(|id| !chat_contacts.contains(id)) {
|
||||
if parent.error.is_some() {
|
||||
// If the parent msg is undecipherable, then it may have been assigned to the wrong chat
|
||||
// (undecipherable group msgs often get assigned to the 1:1 chat with the sender).
|
||||
// We don't have any way of finding out whether a msg is undecipherable, so we check for
|
||||
// error.is_some() instead.
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if parent_chat.id == DC_CHAT_ID_TRASH {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// If this was a private message just to self, it was probably a private reply.
|
||||
// It should not go into the group then, but into the private chat.
|
||||
if is_probably_private_reply(context, to_ids, from_id, mime_parser, parent_chat.id).await? {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// If the parent chat is a 1:1 chat, and the sender is a classical MUA and added
|
||||
// a new person to TO/CC, then the message should not go to the 1:1 chat, but to a
|
||||
// newly created ad-hoc group.
|
||||
if parent_chat.typ == Chattype::Single
|
||||
&& !mime_parser.has_chat_version()
|
||||
&& to_ids.len() > 1
|
||||
{
|
||||
let mut chat_contacts = chat::get_chat_contacts(context, parent_chat.id).await?;
|
||||
chat_contacts.push(ContactId::SELF);
|
||||
if to_ids.iter().any(|id| !chat_contacts.contains(id)) {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Assigning message to {} as it's a reply to {}.", parent_chat.id, parent.rfc724_mid
|
||||
);
|
||||
return Ok(Some((parent_chat.id, parent_chat.blocked)));
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Assigning message to {} as it's a reply to {}.", parent_chat.id, parent.rfc724_mid
|
||||
);
|
||||
Ok(Some((parent_chat.id, parent_chat.blocked)))
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// If this method returns true, the message shall be assigned to the 1:1 chat with the sender.
|
||||
@@ -1661,7 +1686,7 @@ async fn apply_group_changes(
|
||||
}
|
||||
|
||||
let mut send_event_chat_modified = false;
|
||||
let (mut removed_id, mut added_id) = (None, None);
|
||||
let mut removed_id = None;
|
||||
let mut better_msg = None;
|
||||
|
||||
// True if a Delta Chat client has explicitly added our current primary address.
|
||||
@@ -1672,36 +1697,40 @@ async fn apply_group_changes(
|
||||
false
|
||||
};
|
||||
|
||||
let mut chat_contacts = HashSet::from_iter(chat::get_chat_contacts(context, chat_id).await?);
|
||||
let is_from_in_chat =
|
||||
!chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
|
||||
|
||||
// Reject group membership changes from non-members and old changes.
|
||||
let allow_member_list_changes = is_from_in_chat
|
||||
&& chat_id
|
||||
// Whether to allow any changes to the member list at all.
|
||||
let allow_member_list_changes = if chat::is_contact_in_chat(context, chat_id, ContactId::SELF)
|
||||
.await?
|
||||
|| self_added
|
||||
|| !mime_parser.has_chat_version()
|
||||
{
|
||||
// Reject old group changes.
|
||||
chat_id
|
||||
.update_timestamp(context, Param::MemberListTimestamp, sent_timestamp)
|
||||
.await?;
|
||||
.await?
|
||||
} else {
|
||||
// Member list changes are not allowed if we're not in the group
|
||||
// and are not explicitly added.
|
||||
// This message comes from a Delta Chat that restored an old backup
|
||||
// or the message is a MUA reply to an old message.
|
||||
false
|
||||
};
|
||||
|
||||
// Whether to rebuild the member list from scratch.
|
||||
let recreate_member_list = {
|
||||
// Always recreate membership list if SELF has been added. The older versions of DC
|
||||
// don't always set "In-Reply-To" to the latest message they sent, but to the latest
|
||||
// delivered message (so it's a race), so we have this heuristic here.
|
||||
self_added
|
||||
|| match mime_parser.get_header(HeaderDef::InReplyTo) {
|
||||
let recreate_member_list = if allow_member_list_changes {
|
||||
// Recreate member list if the message comes from a MUA as these messages do _not_ set add/remove headers.
|
||||
// Always recreate membership list if self has been added.
|
||||
if !mime_parser.has_chat_version() || self_added {
|
||||
true
|
||||
} else {
|
||||
match mime_parser.get_header(HeaderDef::InReplyTo) {
|
||||
// If we don't know the referenced message, we missed some messages.
|
||||
// Maybe they added/removed members, so we need to recreate our member list.
|
||||
Some(reply_to) => rfc724_mid_exists(context, reply_to).await?.is_none(),
|
||||
None => false,
|
||||
}
|
||||
} && {
|
||||
if !allow_member_list_changes {
|
||||
info!(
|
||||
context,
|
||||
"Ignoring a try to recreate member list of {chat_id} by {from_id}.",
|
||||
);
|
||||
}
|
||||
allow_member_list_changes
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
|
||||
@@ -1713,8 +1742,14 @@ async fn apply_group_changes(
|
||||
Some(stock_str::msg_del_member_local(context, removed_addr, from_id).await)
|
||||
};
|
||||
|
||||
if removed_id.is_some() {
|
||||
if !allow_member_list_changes {
|
||||
if let Some(contact_id) = removed_id {
|
||||
if allow_member_list_changes {
|
||||
// Remove a single member from the chat.
|
||||
if !recreate_member_list {
|
||||
chat::remove_from_chat_contacts_table(context, chat_id, contact_id).await?;
|
||||
send_event_chat_modified = true;
|
||||
}
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"Ignoring removal of {removed_addr:?} from {chat_id}."
|
||||
@@ -1727,11 +1762,13 @@ async fn apply_group_changes(
|
||||
better_msg = Some(stock_str::msg_add_member_local(context, added_addr, from_id).await);
|
||||
|
||||
if allow_member_list_changes {
|
||||
// Add a single member to the chat.
|
||||
if !recreate_member_list {
|
||||
if let Some(contact_id) =
|
||||
Contact::lookup_id_by_addr(context, added_addr, Origin::Unknown).await?
|
||||
{
|
||||
added_id = Some(contact_id);
|
||||
chat::add_to_chat_contacts_table(context, chat_id, &[contact_id]).await?;
|
||||
send_event_chat_modified = true;
|
||||
} else {
|
||||
warn!(context, "Added {added_addr:?} has no contact id.")
|
||||
}
|
||||
@@ -1798,76 +1835,54 @@ async fn apply_group_changes(
|
||||
}
|
||||
}
|
||||
|
||||
if allow_member_list_changes {
|
||||
let mut new_members = HashSet::from_iter(to_ids.iter().copied());
|
||||
new_members.insert(ContactId::SELF);
|
||||
if !from_id.is_special() {
|
||||
new_members.insert(from_id);
|
||||
}
|
||||
|
||||
if !recreate_member_list {
|
||||
let diff: HashSet<ContactId> =
|
||||
chat_contacts.difference(&new_members).copied().collect();
|
||||
// Recreate the member list.
|
||||
if recreate_member_list {
|
||||
if !chat::is_contact_in_chat(context, chat_id, from_id).await? {
|
||||
warn!(
|
||||
context,
|
||||
"Contact {from_id} attempts to modify group chat {chat_id} member list without being a member."
|
||||
);
|
||||
} else {
|
||||
// Only delete old contacts if the sender is not a classical MUA user:
|
||||
// Classical MUA users usually don't intend to remove users from an email
|
||||
// thread, so if they removed a recipient then it was probably by accident.
|
||||
if mime_parser.has_chat_version() {
|
||||
// This is what provides group membership consistency: we remove group members
|
||||
// locally if we see a discrepancy with the "To" list in the received message as it
|
||||
// is better for privacy than adding absent members locally. But it shouldn't be a
|
||||
// big problem if somebody missed a member addition, because they will likely
|
||||
// recreate the member list from the next received message. The problem occurs only
|
||||
// if that "somebody" managed to reply earlier. Really, it's a problem for big
|
||||
// groups with high message rate, but let it be for now.
|
||||
if !diff.is_empty() {
|
||||
warn!(context, "Implicit removal of {diff:?} from chat {chat_id}.");
|
||||
}
|
||||
new_members = chat_contacts.difference(&diff).copied().collect();
|
||||
} else {
|
||||
new_members.extend(diff);
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM chats_contacts WHERE chat_id=?;", (chat_id,))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
if let Some(removed_id) = removed_id {
|
||||
new_members.remove(&removed_id);
|
||||
}
|
||||
if let Some(added_id) = added_id {
|
||||
new_members.insert(added_id);
|
||||
}
|
||||
if recreate_member_list {
|
||||
|
||||
let mut members_to_add = HashSet::new();
|
||||
members_to_add.extend(to_ids);
|
||||
members_to_add.insert(ContactId::SELF);
|
||||
|
||||
if !from_id.is_special() {
|
||||
members_to_add.insert(from_id);
|
||||
}
|
||||
|
||||
if let Some(removed_id) = removed_id {
|
||||
members_to_add.remove(&removed_id);
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Recreating chat {chat_id} member list with {new_members:?}.",
|
||||
"Recreating chat {chat_id} with members {members_to_add:?}."
|
||||
);
|
||||
}
|
||||
|
||||
if new_members != chat_contacts {
|
||||
let new_members_ref = &new_members;
|
||||
context
|
||||
.sql
|
||||
.transaction(move |transaction| {
|
||||
transaction
|
||||
.execute("DELETE FROM chats_contacts WHERE chat_id=?", (chat_id,))?;
|
||||
for contact_id in new_members_ref {
|
||||
transaction.execute(
|
||||
"INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
|
||||
(chat_id, contact_id),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
chat::add_to_chat_contacts_table(context, chat_id, &Vec::from_iter(members_to_add))
|
||||
.await?;
|
||||
chat_contacts = new_members;
|
||||
send_event_chat_modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(avatar_action) = &mime_parser.group_avatar {
|
||||
if !chat_contacts.contains(&ContactId::SELF) {
|
||||
if !chat::is_contact_in_chat(context, chat_id, ContactId::SELF).await? {
|
||||
warn!(
|
||||
context,
|
||||
"Received group avatar update for group chat {chat_id} we are not a member of."
|
||||
);
|
||||
} else if !chat_contacts.contains(&from_id) {
|
||||
} else if !chat::is_contact_in_chat(context, chat_id, from_id).await? {
|
||||
warn!(
|
||||
context,
|
||||
"Contact {from_id} attempts to modify group chat {chat_id} avatar without being a member.",
|
||||
@@ -2028,40 +2043,39 @@ async fn apply_mailinglist_changes(
|
||||
mime_parser: &MimeMessage,
|
||||
chat_id: ChatId,
|
||||
) -> Result<()> {
|
||||
let Some(list_post) = &mime_parser.list_post else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
if chat.typ != Chattype::Mailinglist {
|
||||
return Ok(());
|
||||
}
|
||||
let listid = &chat.grpid;
|
||||
|
||||
let list_post = match ContactAddress::new(list_post) {
|
||||
Ok(list_post) => list_post,
|
||||
Err(err) => {
|
||||
warn!(context, "Invalid List-Post: {:#}.", err);
|
||||
if let Some(list_post) = &mime_parser.list_post {
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
if chat.typ != Chattype::Mailinglist {
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let (contact_id, _) = Contact::add_or_lookup(context, "", list_post, Origin::Hidden).await?;
|
||||
let mut contact = Contact::get_by_id(context, contact_id).await?;
|
||||
if contact.param.get(Param::ListId) != Some(listid) {
|
||||
contact.param.set(Param::ListId, listid);
|
||||
contact.update_param(context).await?;
|
||||
}
|
||||
let listid = &chat.grpid;
|
||||
|
||||
if let Some(old_list_post) = chat.param.get(Param::ListPost) {
|
||||
if list_post.as_ref() != old_list_post {
|
||||
// Apparently the mailing list is using a different List-Post header in each message.
|
||||
// Make the mailing list read-only because we wouldn't know which message the user wants to reply to.
|
||||
chat.param.remove(Param::ListPost);
|
||||
let list_post = match ContactAddress::new(list_post) {
|
||||
Ok(list_post) => list_post,
|
||||
Err(err) => {
|
||||
warn!(context, "Invalid List-Post: {:#}.", err);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let (contact_id, _) =
|
||||
Contact::add_or_lookup(context, "", list_post, Origin::Hidden).await?;
|
||||
let mut contact = Contact::get_by_id(context, contact_id).await?;
|
||||
if contact.param.get(Param::ListId) != Some(listid) {
|
||||
contact.param.set(Param::ListId, listid);
|
||||
contact.update_param(context).await?;
|
||||
}
|
||||
|
||||
if let Some(old_list_post) = chat.param.get(Param::ListPost) {
|
||||
if list_post.as_ref() != old_list_post {
|
||||
// Apparently the mailing list is using a different List-Post header in each message.
|
||||
// Make the mailing list read-only because we wouldn't know which message the user wants to reply to.
|
||||
chat.param.remove(Param::ListPost);
|
||||
chat.update_param(context).await?;
|
||||
}
|
||||
} else {
|
||||
chat.param.set(Param::ListPost, list_post);
|
||||
chat.update_param(context).await?;
|
||||
}
|
||||
} else {
|
||||
chat.param.set(Param::ListPost, list_post);
|
||||
chat.update_param(context).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -8,7 +8,6 @@ use crate::chat::{
|
||||
};
|
||||
use crate::chat::{get_chat_msgs, ChatItem, ChatVisibility};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS};
|
||||
use crate::imap::prefetch_should_download;
|
||||
use crate::message::Message;
|
||||
@@ -3369,14 +3368,19 @@ async fn test_dont_recreate_contacts_on_add_remove() -> Result<()> {
|
||||
|
||||
alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
|
||||
// Bob didn't receive the addition of Fiona, so Alice must remove Fiona from the members list
|
||||
// back to make their group members view consistent.
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 3);
|
||||
// bob didn't receive the addition of fiona, but alice doesn't overwrite her own
|
||||
// contact list with the one from bob which only has three members instead of four.
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 4);
|
||||
|
||||
// Just a dumb check for remove_contact_from_chat(). Let's have it in this only place.
|
||||
// bob removes a member.
|
||||
remove_contact_from_chat(&bob, bob_chat_id, bob_blue).await?;
|
||||
|
||||
alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2);
|
||||
|
||||
// Bobs chat only has two members after the removal of blue, because he still
|
||||
// didn't receive the addition of fiona. But that doesn't overwrite alice'
|
||||
// memberlist.
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 3);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -3509,29 +3513,6 @@ async fn test_mua_cant_remove() -> Result<()> {
|
||||
chat::get_chat_contacts(&alice, group_chat.id).await?.len(),
|
||||
4
|
||||
);
|
||||
|
||||
// But if the parent message is missing, the message must goto a new ad-hoc group.
|
||||
let bob_removes = receive_imf(
|
||||
&alice,
|
||||
b"Subject: Re: Message from alice\r\n\
|
||||
From: <bob@example.net>\r\n\
|
||||
To: <alice@example.org>, <claire@example.org>\r\n\
|
||||
Date: Mon, 12 Dec 2022 14:32:40 +0000\r\n\
|
||||
Message-ID: <bobs_answer_to_two_recipients_1@example.net>\r\n\
|
||||
In-Reply-To: <Mr.missing@example.org>\r\n\
|
||||
\r\n\
|
||||
Hi back!\r\n",
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_ne!(bob_removes.chat_id, alice_chat.id);
|
||||
let group_chat = Chat::load_from_db(&alice, bob_removes.chat_id).await?;
|
||||
assert_eq!(group_chat.typ, Chattype::Group);
|
||||
assert_eq!(
|
||||
chat::get_chat_contacts(&alice, group_chat.id).await?.len(),
|
||||
3,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3630,70 +3611,3 @@ async fn test_mua_can_readd() -> Result<()> {
|
||||
assert!(is_contact_in_chat(&alice, alice_chat.id, ContactId::SELF).await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recreate_member_list_on_missing_add_of_self() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_chat_id,
|
||||
Contact::create(&alice, "bob", &bob.get_config(Config::Addr).await?.unwrap()).await?,
|
||||
)
|
||||
.await?;
|
||||
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
|
||||
alice.pop_sent_msg().await;
|
||||
remove_contact_from_chat(&alice, alice_chat_id, ContactId::SELF).await?;
|
||||
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
|
||||
|
||||
// Bob missed the message adding them, but must recreate the member list.
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 1);
|
||||
assert!(is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_keep_member_list_if_possibly_nomember() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_chat_id,
|
||||
Contact::create(&alice, "bob", &bob.get_config(Config::Addr).await?.unwrap()).await?,
|
||||
)
|
||||
.await?;
|
||||
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
|
||||
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
|
||||
|
||||
let fiona = TestContext::new_fiona().await;
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_chat_id,
|
||||
Contact::create(
|
||||
&alice,
|
||||
"fiona",
|
||||
&fiona.get_config(Config::Addr).await?.unwrap(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
.await?;
|
||||
let fiona_chat_id = fiona.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
|
||||
fiona_chat_id.accept(&fiona).await?;
|
||||
|
||||
send_text_msg(&fiona, fiona_chat_id, "hi".to_string()).await?;
|
||||
bob.recv_msg(&fiona.pop_sent_msg().await).await;
|
||||
|
||||
// Bob missed the message adding fiona, but mustn't recreate the member list.
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
|
||||
assert!(is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
|
||||
let bob_alice_contact = Contact::create(
|
||||
&bob,
|
||||
"alice",
|
||||
&alice.get_config(Config::Addr).await?.unwrap(),
|
||||
)
|
||||
.await?;
|
||||
assert!(is_contact_in_chat(&bob, bob_chat_id, bob_alice_contact).await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use core::fmt;
|
||||
use std::cmp::min;
|
||||
use std::{iter::once, ops::Deref, sync::Arc};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
@@ -458,8 +457,7 @@ impl Context {
|
||||
} else {
|
||||
"green"
|
||||
};
|
||||
let div_width_percent = min(100, percent);
|
||||
ret += &format!("<div class=\"bar\"><div class=\"progress {color}\" style=\"width: {div_width_percent}%\">{percent}%</div></div>");
|
||||
ret += &format!("<div class=\"bar\"><div class=\"progress {color}\" style=\"width: {percent}%\">{percent}%</div></div>");
|
||||
|
||||
ret += "</li>";
|
||||
}
|
||||
|
||||
36
src/sql.rs
36
src/sql.rs
@@ -310,17 +310,12 @@ impl Sql {
|
||||
/// It is impossible to turn encrypted database into unencrypted
|
||||
/// and vice versa this way, use import/export for this.
|
||||
pub async fn change_passphrase(&self, passphrase: String) -> Result<()> {
|
||||
let mut lock = self.pool.write().await;
|
||||
|
||||
let pool = lock.take().context("SQL connection pool is not open")?;
|
||||
let conn = pool.get().await?;
|
||||
conn.pragma_update(None, "rekey", passphrase.clone())
|
||||
.context("failed to set PRAGMA rekey")?;
|
||||
drop(pool);
|
||||
|
||||
*lock = Some(Self::new_pool(&self.dbfile, passphrase.to_string())?);
|
||||
|
||||
Ok(())
|
||||
self.call_write(move |conn| {
|
||||
conn.pragma_update(None, "rekey", passphrase)
|
||||
.context("failed to set PRAGMA rekey")?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Locks the write transactions mutex in order to make sure that there never are
|
||||
@@ -1270,7 +1265,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sql_change_passphrase() -> Result<()> {
|
||||
async fn test_change_passphrase() -> Result<()> {
|
||||
use tempfile::tempdir;
|
||||
|
||||
// The context is used only for logging.
|
||||
@@ -1294,23 +1289,6 @@ mod tests {
|
||||
sql.change_passphrase("bar".to_string())
|
||||
.await
|
||||
.context("failed to change passphrase")?;
|
||||
|
||||
// Test that at least two connections are still working.
|
||||
// This ensures that not only the connection which changed the password is working,
|
||||
// but other connections as well.
|
||||
{
|
||||
let lock = sql.pool.read().await;
|
||||
let pool = lock.as_ref().unwrap();
|
||||
let conn1 = pool.get().await?;
|
||||
let conn2 = pool.get().await?;
|
||||
conn1
|
||||
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
|
||||
.unwrap();
|
||||
conn2
|
||||
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
sql.close().await;
|
||||
|
||||
let sql = Sql::new(dbfile);
|
||||
|
||||
@@ -108,15 +108,9 @@ impl TestContextManager {
|
||||
/// - Let one TestContext send a message
|
||||
/// - Let the other TestContext receive it and accept the chat
|
||||
/// - Assert that the message arrived
|
||||
pub async fn send_recv_accept(
|
||||
&self,
|
||||
from: &TestContext,
|
||||
to: &TestContext,
|
||||
msg: &str,
|
||||
) -> Message {
|
||||
pub async fn send_recv_accept(&self, from: &TestContext, to: &TestContext, msg: &str) {
|
||||
let received_msg = self.send_recv(from, to, msg).await;
|
||||
received_msg.chat_id.accept(to).await.unwrap();
|
||||
received_msg
|
||||
}
|
||||
|
||||
/// - Let one TestContext send a message
|
||||
|
||||
260
src/webxdc.rs
260
src/webxdc.rs
@@ -26,7 +26,8 @@ use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use crate::chat::Chat;
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{self, create_send_msg_job, Chat};
|
||||
use crate::constants::Chattype;
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
@@ -34,12 +35,12 @@ use crate::download::DownloadState;
|
||||
use crate::message::{Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimefactory::wrapped_base64_encode;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Param;
|
||||
use crate::param::Params;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::scheduler::InterruptInfo;
|
||||
use crate::tools::strip_rtlo_characters;
|
||||
use crate::tools::{create_smeared_timestamp, get_abs_path};
|
||||
use crate::{chat, EventType};
|
||||
use crate::tools::{
|
||||
create_outgoing_rfc724_mid, create_smeared_timestamp, get_abs_path, strip_rtlo_characters,
|
||||
};
|
||||
use crate::EventType;
|
||||
|
||||
/// The current API version.
|
||||
/// If `min_api` in manifest.toml is set to a larger value,
|
||||
@@ -845,6 +846,77 @@ impl Message {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a replacement for an own WebXDC message.
|
||||
pub async fn send_webxdc_replacement(
|
||||
context: &Context,
|
||||
msg_id: MsgId,
|
||||
filename: &str,
|
||||
) -> Result<()> {
|
||||
let mut msg = Message::load_from_db(context, msg_id).await?;
|
||||
|
||||
ensure!(
|
||||
msg.from_id == ContactId::SELF,
|
||||
"Can update WebXDC only in own messages"
|
||||
);
|
||||
ensure!(
|
||||
msg.get_viewtype() == Viewtype::Webxdc,
|
||||
"Message {msg_id} is not a WebXDC instance"
|
||||
);
|
||||
let state = msg.get_state();
|
||||
match state {
|
||||
MessageState::OutFailed | MessageState::OutDelivered | MessageState::OutMdnRcvd => {}
|
||||
MessageState::Undefined
|
||||
| MessageState::InFresh
|
||||
| MessageState::InNoticed
|
||||
| MessageState::InSeen
|
||||
| MessageState::OutPreparing
|
||||
| MessageState::OutPending
|
||||
| MessageState::OutDraft => bail!("Unexpected message state: {state}"),
|
||||
}
|
||||
|
||||
let chat = Chat::load_from_db(context, msg.chat_id).await?;
|
||||
|
||||
let mut param = msg.param.clone();
|
||||
if !chat.is_protected() {
|
||||
param.remove(Param::GuaranteeE2ee);
|
||||
}
|
||||
let blob = BlobObject::new_from_path(context, Path::new(filename))
|
||||
.await
|
||||
.context("Failed to create webxdc replacement blob")?;
|
||||
param.set(Param::File, blob.as_name());
|
||||
msg.param = param;
|
||||
|
||||
// Generate new Message-ID.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs
|
||||
SET state=?, param=?
|
||||
WHERE id=?",
|
||||
(MessageState::OutPending, msg.param.to_string(), msg_id),
|
||||
)
|
||||
.await?;
|
||||
|
||||
msg.supersedes = Some(msg.rfc724_mid);
|
||||
msg.rfc724_mid = {
|
||||
let grpid = match chat.typ {
|
||||
Chattype::Group => Some(chat.grpid.as_str()),
|
||||
_ => None,
|
||||
};
|
||||
let from = context.get_primary_self_addr().await?;
|
||||
create_outgoing_rfc724_mid(grpid, &from)
|
||||
};
|
||||
|
||||
if create_send_msg_job(context, &mut msg).await?.is_some() {
|
||||
context
|
||||
.scheduler
|
||||
.interrupt_smtp(InterruptInfo::new(false))
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
@@ -1012,7 +1084,7 @@ mod tests {
|
||||
let instance = send_webxdc_instance(&t, chat_id).await?;
|
||||
t.send_webxdc_status_update(
|
||||
instance.id,
|
||||
r#"{"info": "foo", "summary":"bar", "document":"doc", "payload": 42}"#,
|
||||
r#"{"info": "foo", "summary":"bar", "payload": 42}"#,
|
||||
"descr",
|
||||
)
|
||||
.await?;
|
||||
@@ -1020,7 +1092,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
|
||||
.await?,
|
||||
r#"[{"payload":42,"info":"foo","document":"doc","summary":"bar","serial":1,"max_serial":1}]"#
|
||||
r#"[{"payload":42,"info":"foo","summary":"bar","serial":1,"max_serial":1}]"#
|
||||
);
|
||||
assert_eq!(chat_id.get_msg_cnt(&t).await?, 2); // instance and info
|
||||
let info = Message::load_from_db(&t, instance.id)
|
||||
@@ -1028,7 +1100,6 @@ mod tests {
|
||||
.get_webxdc_info(&t)
|
||||
.await?;
|
||||
assert_eq!(info.summary, "bar".to_string());
|
||||
assert_eq!(info.document, "doc".to_string());
|
||||
|
||||
// forwarding an instance creates a fresh instance; updates etc. are not forwarded
|
||||
forward_msgs(&t, &[instance.get_id()], chat_id).await?;
|
||||
@@ -1045,7 +1116,6 @@ mod tests {
|
||||
.get_webxdc_info(&t)
|
||||
.await?;
|
||||
assert_eq!(info.summary, "".to_string());
|
||||
assert_eq!(info.document, "".to_string());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2625,4 +2695,174 @@ sth_for_the = "future""#
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests sending webxdc and replacing it with a newer version.
|
||||
///
|
||||
/// Updates should be preserved after upgrading.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_replace_webxdc() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
// Alice sends WebXDC instance.
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
let mut alice_instance = create_webxdc_instance(
|
||||
&alice,
|
||||
"minimal.xdc",
|
||||
include_bytes!("../test-data/webxdc/minimal.xdc"),
|
||||
)
|
||||
.await?;
|
||||
alice_instance.set_text("user added text".to_string());
|
||||
send_msg(&alice, alice_chat.id, &mut alice_instance).await?;
|
||||
let alice_instance = alice.get_last_msg().await;
|
||||
assert_eq!(alice_instance.get_text(), "user added text");
|
||||
let original_rfc724_mid = alice_instance.rfc724_mid;
|
||||
|
||||
// Bob receives that instance.
|
||||
let alice_sent_instance = alice.pop_sent_msg().await;
|
||||
let bob_received_instance = bob.recv_msg(&alice_sent_instance).await;
|
||||
assert_eq!(bob_received_instance.get_text(), "user added text");
|
||||
|
||||
// Alice sends WebXDC update.
|
||||
alice
|
||||
.send_webxdc_status_update(alice_instance.id, r#"{"payload": 1}"#, "Alice update")
|
||||
.await?;
|
||||
alice.flush_status_updates().await?;
|
||||
let alice_sent_update = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&alice_sent_update).await;
|
||||
assert_eq!(
|
||||
bob.get_webxdc_status_updates(bob_received_instance.id, StatusUpdateSerial(0))
|
||||
.await?,
|
||||
r#"[{"payload":1,"serial":1,"max_serial":1}]"#
|
||||
);
|
||||
|
||||
// Alice sends WebXDC instance replacement.
|
||||
send_webxdc_replacement(
|
||||
&alice,
|
||||
alice_instance.id,
|
||||
"test-data/webxdc/with-minimal-manifest.xdc",
|
||||
)
|
||||
.await
|
||||
.context("Failed to send WebXDC replacement")?;
|
||||
let alice_replacement_instance = alice.get_last_msg().await;
|
||||
let alice_replacement_info = alice_replacement_instance.get_webxdc_info(&alice).await?;
|
||||
assert_eq!(alice_replacement_info.name, "nice app!");
|
||||
assert_eq!(alice_instance.id, alice_replacement_instance.id);
|
||||
let alice_sent_replacement_instance = alice.pop_sent_msg().await;
|
||||
assert!(alice_sent_replacement_instance
|
||||
.payload
|
||||
.contains(&format!("Supersedes: {original_rfc724_mid}")));
|
||||
assert_eq!(alice_replacement_instance.rfc724_mid, original_rfc724_mid);
|
||||
|
||||
// Bob receives WebXDC instance replacement.
|
||||
let bob_received_replacement_instance =
|
||||
bob.recv_msg(&alice_sent_replacement_instance).await;
|
||||
assert_eq!(
|
||||
bob_received_instance.id,
|
||||
bob_received_replacement_instance.id
|
||||
);
|
||||
assert_eq!(
|
||||
bob_received_replacement_instance.rfc724_mid,
|
||||
original_rfc724_mid
|
||||
);
|
||||
let bob_received_replacement_info = bob_received_replacement_instance
|
||||
.get_webxdc_info(&bob)
|
||||
.await?;
|
||||
assert_eq!(bob_received_replacement_info.name, "nice app!");
|
||||
|
||||
// Updates are not modified.
|
||||
assert_eq!(
|
||||
bob.get_webxdc_status_updates(
|
||||
bob_received_replacement_instance.id,
|
||||
StatusUpdateSerial(0)
|
||||
)
|
||||
.await?,
|
||||
r#"[{"payload":1,"serial":1,"max_serial":1}]"#
|
||||
);
|
||||
|
||||
// Bob is not allowed to replace the instance.
|
||||
assert!(send_webxdc_replacement(
|
||||
&bob,
|
||||
bob_received_instance.id,
|
||||
"test-data/webxdc/minimal.xdc"
|
||||
)
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
// Alice sends a second WebXDC instance replacement.
|
||||
send_webxdc_replacement(&alice, alice_instance.id, "test-data/webxdc/minimal.xdc")
|
||||
.await
|
||||
.context("Failed to send second WebXDC replacement")?;
|
||||
let alice_second_sent_replacement_instance = alice.pop_sent_msg().await;
|
||||
let bob_received_second_replacement_instance =
|
||||
bob.recv_msg(&alice_second_sent_replacement_instance).await;
|
||||
assert_eq!(
|
||||
bob_received_instance.id,
|
||||
bob_received_second_replacement_instance.id
|
||||
);
|
||||
assert_eq!(
|
||||
bob_received_second_replacement_instance.rfc724_mid,
|
||||
original_rfc724_mid
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_replace_webxdc_missing_original() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
// Alice sends WebXDC instance.
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
let mut alice_instance = create_webxdc_instance(
|
||||
&alice,
|
||||
"minimal.xdc",
|
||||
include_bytes!("../test-data/webxdc/minimal.xdc"),
|
||||
)
|
||||
.await?;
|
||||
alice_instance.set_text("user added text".to_string());
|
||||
send_msg(&alice, alice_chat.id, &mut alice_instance).await?;
|
||||
alice.pop_sent_msg().await;
|
||||
let alice_instance = alice.get_last_msg().await;
|
||||
assert_eq!(alice_instance.get_text(), "user added text");
|
||||
let original_rfc724_mid = alice_instance.rfc724_mid;
|
||||
|
||||
// Bob missed the original instance message.
|
||||
|
||||
// Alice sends WebXDC instance replacement.
|
||||
send_webxdc_replacement(&alice, alice_instance.id, "test-data/webxdc/minimal.xdc")
|
||||
.await
|
||||
.context("Failed to send WebXDC replacement")?;
|
||||
let alice_sent_replacement_instance = alice.pop_sent_msg().await;
|
||||
assert!(alice_sent_replacement_instance
|
||||
.payload
|
||||
.contains(&format!("Supersedes: {original_rfc724_mid}")));
|
||||
|
||||
// Bob receives WebXDC instance replacement.
|
||||
let bob_received_replacement_instance =
|
||||
bob.recv_msg(&alice_sent_replacement_instance).await;
|
||||
assert_eq!(
|
||||
bob_received_replacement_instance.rfc724_mid,
|
||||
original_rfc724_mid
|
||||
);
|
||||
|
||||
// Alice sends a second WebXDC instance replacement.
|
||||
send_webxdc_replacement(&alice, alice_instance.id, "test-data/webxdc/minimal.xdc")
|
||||
.await
|
||||
.context("Failed to send second WebXDC replacement")?;
|
||||
let alice_second_sent_replacement_instance = alice.pop_sent_msg().await;
|
||||
let bob_received_second_replacement_instance =
|
||||
bob.recv_msg(&alice_second_sent_replacement_instance).await;
|
||||
assert_eq!(
|
||||
bob_received_replacement_instance.id,
|
||||
bob_received_second_replacement_instance.id
|
||||
);
|
||||
assert_eq!(
|
||||
bob_received_second_replacement_instance.rfc724_mid,
|
||||
original_rfc724_mid
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user