mirror of
https://github.com/chatmail/core.git
synced 2026-10-18 02:43:02 +03:00
Compare commits
79
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bb6b89f1b | ||
|
|
bb5b97f168 | ||
|
|
1856c622a1 | ||
|
|
0a48a2effa | ||
|
|
543864f0f5 | ||
|
|
0fe94e47cc | ||
|
|
fc09210aea | ||
|
|
3e194969c0 | ||
|
|
391cffb454 | ||
|
|
47486f8bab | ||
|
|
620e363ce6 | ||
|
|
0c2276775d | ||
|
|
ad51a7cd85 | ||
|
|
28952789a4 | ||
|
|
14adcdb517 | ||
|
|
cc80590488 | ||
|
|
48416289ac | ||
|
|
003a27f625 | ||
|
|
bff4a2259f | ||
|
|
9adf856705 | ||
|
|
2215de5285 | ||
|
|
013467d6c6 | ||
|
|
1f52b8af2f | ||
|
|
ce32f76265 | ||
|
|
836f65376c | ||
|
|
99940dd28c | ||
|
|
ffeb801b58 | ||
|
|
339bbcf070 | ||
|
|
7b83bddc2d | ||
|
|
5549733a0b | ||
|
|
4e21917c0e | ||
|
|
939b4b2aab | ||
|
|
fd0770859d | ||
|
|
d5854fb3c9 | ||
|
|
3aa22a27cc | ||
|
|
10b1a2f5f5 | ||
|
|
a28a34773c | ||
|
|
7c744d14d7 | ||
|
|
6c68f2eb7e | ||
|
|
eb2d2b7313 | ||
|
|
2e50abedaa | ||
|
|
ee53136ed2 | ||
|
|
e923983dca | ||
|
|
f4753862f1 | ||
|
|
16b40f3a19 | ||
|
|
9cd3a7550b | ||
|
|
d840a7e6b9 | ||
|
|
d875691955 | ||
|
|
c600bfa8ca | ||
|
|
973ffa1a64 | ||
|
|
caffc3d93c | ||
|
|
a4dcf656f3 | ||
|
|
7bb5d48966 | ||
|
|
71b7b0b393 | ||
|
|
bd02eea66b | ||
|
|
cdcb10fb58 | ||
|
|
6cd7296001 | ||
|
|
168021523f | ||
|
|
03f2635296 | ||
|
|
e3b08fa92b | ||
|
|
79cebe66de | ||
|
|
0619e2a129 | ||
|
|
64a81e4f61 | ||
|
|
0431ae53ca | ||
|
|
89c873acd0 | ||
|
|
2e70cf9388 | ||
|
|
2efd0461d1 | ||
|
|
196a34684d | ||
|
|
402fd6850c | ||
|
|
72515f440d | ||
|
|
045d919cdc | ||
|
|
9b9108320e | ||
|
|
5efb100f12 | ||
|
|
b747dd6ae8 | ||
|
|
ed2bc9e44d | ||
|
|
9e1a2149fa | ||
|
|
22b6d8c17b | ||
|
|
3876846410 | ||
|
|
a93c79e001 |
@@ -17,7 +17,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "16"
|
||||
node-version: "18"
|
||||
- name: Get tag
|
||||
id: tag
|
||||
uses: dawidd6/action-get-tag@v1
|
||||
|
||||
@@ -15,10 +15,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js 16.x
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
node-version: 18.x
|
||||
- name: Add Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: npm install
|
||||
|
||||
@@ -16,10 +16,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js 16.x
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
node-version: 18.x
|
||||
|
||||
- name: npm install and generate documentation
|
||||
working-directory: node
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "16"
|
||||
node-version: "18"
|
||||
- name: System info
|
||||
run: |
|
||||
rustc -vV
|
||||
@@ -66,10 +66,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Build Linux prebuilds inside a container with old glibc for backwards compatibility.
|
||||
# Debian 10 contained glibc 2.28 at the time of the writing (2023-06-04): https://packages.debian.org/buster/libc6
|
||||
# Ubuntu 18.04 is at the End of Standard Support since June 2023, but it contains glibc 2.27,
|
||||
# so we are using it to support Ubuntu 18.04 setups that are still not upgraded.
|
||||
container: ubuntu:18.04
|
||||
# Debian 10 contained glibc 2.28: https://packages.debian.org/buster/libc6
|
||||
container: debian:10
|
||||
steps:
|
||||
# Working directory is owned by 1001:1001 by default.
|
||||
# Change it to our user.
|
||||
@@ -80,7 +78,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "16"
|
||||
node-version: "18"
|
||||
- run: apt-get update
|
||||
|
||||
# Python is needed for node-gyp
|
||||
@@ -143,7 +141,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: "16"
|
||||
node-version: "18"
|
||||
- name: Get tag
|
||||
id: tag
|
||||
uses: dawidd6/action-get-tag@v1
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "16"
|
||||
node-version: "18"
|
||||
- name: System info
|
||||
run: |
|
||||
rustc -vV
|
||||
|
||||
@@ -1,5 +1,67 @@
|
||||
# Changelog
|
||||
|
||||
## [1.129.1] - 2023-11-06
|
||||
|
||||
### Fixes
|
||||
|
||||
- Update tokio-imap to fix Outlook STATUS parsing bug.
|
||||
- deltachat-rpc-client: Add the Lock around request ID.
|
||||
- `apply_group_changes`: Don't implicitly delete members locally, add absent ones instead ([#4934](https://github.com/deltachat/deltachat-core-rust/pull/4934)).
|
||||
- Partial messages do not change group state ([#4900](https://github.com/deltachat/deltachat-core-rust/pull/4900)).
|
||||
|
||||
### Tests
|
||||
|
||||
- Group chats device synchronisation.
|
||||
|
||||
## [1.129.0] - 2023-11-06
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add JSON-RPC `get_chat_id_by_contact_id` API ([#4918](https://github.com/deltachat/deltachat-core-rust/pull/4918)).
|
||||
- [**breaking**] Remove deprecated `get_verifier_addr`.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Sync chat `Blocked` state, chat visibility, chat mute duration and contact blocked status across devices ([#4817](https://github.com/deltachat/deltachat-core-rust/pull/4817)).
|
||||
- Add 'group created instructions' as info message ([#4916](https://github.com/deltachat/deltachat-core-rust/pull/4916)).
|
||||
- Add hardcoded fallback DNS cache.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Switch to `EncryptionPreference::Mutual` on a receipt of encrypted+signed message ([#4707](https://github.com/deltachat/deltachat-core-rust/pull/4707)).
|
||||
- imap: Check UIDNEXT with a STATUS command before going IDLE.
|
||||
- Allow to change verified key via "member added" message.
|
||||
- json-rpc: Return verifier even if the contact is not "verified" (Autocrypt key does not equal Secure-Join key).
|
||||
|
||||
### Documentation
|
||||
|
||||
- Refine `Contact::get_verifier_id` and `Contact::is_verified` documentation ([#4922](https://github.com/deltachat/deltachat-core-rust/pull/4922)).
|
||||
- Contact profile view should not use `dc_contact_is_verified()`.
|
||||
- Remove documentation for non-existing `dc_accounts_new` `os_name` param.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove unused or useless code paths in Secure-Join ([#4897](https://github.com/deltachat/deltachat-core-rust/pull/4897)).
|
||||
- Improve error handling in Secure-Join code.
|
||||
- Add hostname to "no DNS resolution results" error message.
|
||||
- Accept `&str` instead of `Option<String>` in idle().
|
||||
|
||||
## [1.128.0] - 2023-11-02
|
||||
|
||||
### Build system
|
||||
- [**breaking**] Upgrade nodejs version to 18 ([#4903](https://github.com/deltachat/deltachat-core-rust/pull/4903)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- deltachat-rpc-client: Add `Account.wait_for_incoming_msg_event()`.
|
||||
- Decrease ratelimit for .testrun.org subdomains.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not fail securejoin due to unrelated pending bobstate ([#4896](https://github.com/deltachat/deltachat-core-rust/pull/4896)).
|
||||
- Allow other verified group recipients to be unverified, only check the sender verification.
|
||||
- Remove not working attempt to recover from verified key changes.
|
||||
|
||||
## [1.127.2] - 2023-10-29
|
||||
|
||||
### API-Changes
|
||||
@@ -36,6 +98,8 @@
|
||||
- [**breaking**] Remove unused `dc_set_chat_protection()`
|
||||
- Hide `DcSecretKey` trait from the API.
|
||||
- Verified 1:1 chats ([#4315](https://github.com/deltachat/deltachat-core-rust/pull/4315)). Disabled by default, enable with `verified_one_on_one_chats` config.
|
||||
- Add api `chat::Chat::is_protection_broken`
|
||||
- Add `dc_chat_is_protection_broken()` C API.
|
||||
|
||||
### CI
|
||||
|
||||
@@ -3049,3 +3113,6 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.127.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.126.1...v1.127.0
|
||||
[1.127.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.127.0...v1.127.1
|
||||
[1.127.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.127.1...v1.127.2
|
||||
[1.128.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.127.2...v1.128.0
|
||||
[1.129.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.128.0...v1.129.0
|
||||
[1.129.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.129.0...v1.129.1
|
||||
|
||||
Generated
+162
-153
@@ -198,12 +198,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "336d835910fab747186c56586562cb46f42809c2843ef3a84f47509009522838"
|
||||
checksum = "d37875bd9915b7d67c2f117ea2c30a0989874d0b2cb694fe25403c85763c0c9e"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener 3.0.0",
|
||||
"event-listener 3.0.1",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
@@ -224,11 +224,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-imap"
|
||||
version = "0.9.2"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "936c1b580be4373b48c9c687e0c79285441664398354df28d0860087cac0c069"
|
||||
checksum = "2e542b1682eba6b85a721daef0c58e79319ffd0c678565c07ac57c8071c548b5"
|
||||
dependencies = [
|
||||
"async-channel 1.9.0",
|
||||
"async-channel 2.1.0",
|
||||
"base64 0.21.5",
|
||||
"bytes",
|
||||
"chrono",
|
||||
@@ -290,7 +290,7 @@ checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.38",
|
||||
"syn 2.0.39",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -512,9 +512,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brotli-decompressor"
|
||||
version = "2.5.0"
|
||||
version = "2.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da74e2b81409b1b743f8f0c62cc6254afefb8b8e50bbfe3735550f7aeefa3448"
|
||||
checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
@@ -824,9 +824,9 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.10"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fbc60abd742b35f2492f808e1abbb83d45f72db402e14c55057edc9c7b1e9e4"
|
||||
checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
@@ -983,13 +983,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek-derive"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b"
|
||||
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.38",
|
||||
"syn 2.0.39",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1087,11 +1087,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.127.2"
|
||||
version = "1.129.1"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
"async-channel 2.0.0",
|
||||
"async-channel 2.1.0",
|
||||
"async-imap",
|
||||
"async-native-tls",
|
||||
"async-smtp",
|
||||
@@ -1165,10 +1165,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.127.2"
|
||||
version = "1.129.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.0.0",
|
||||
"async-channel 2.1.0",
|
||||
"axum",
|
||||
"base64 0.21.5",
|
||||
"deltachat",
|
||||
@@ -1189,7 +1189,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "1.127.2"
|
||||
version = "1.129.1"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -1204,7 +1204,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.127.2"
|
||||
version = "1.129.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1224,12 +1224,12 @@ name = "deltachat_derive"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.38",
|
||||
"syn 2.0.39",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.127.2"
|
||||
version = "1.129.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1433,7 +1433,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.38",
|
||||
"syn 2.0.39",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1461,9 +1461,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.14"
|
||||
version = "1.0.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23d2f3407d9a573d666de4b5bdf10569d73ca9478087346697dcbae6244bfbcd"
|
||||
checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d"
|
||||
|
||||
[[package]]
|
||||
name = "ecdsa"
|
||||
@@ -1612,6 +1612,12 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embedded-io"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
|
||||
|
||||
[[package]]
|
||||
name = "encoded-words"
|
||||
version = "0.2.0"
|
||||
@@ -1720,7 +1726,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.38",
|
||||
"syn 2.0.39",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1733,7 +1739,7 @@ dependencies = [
|
||||
"num-traits",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.38",
|
||||
"syn 2.0.39",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1792,9 +1798,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "3.0.0"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29e56284f00d94c1bc7fd3c77027b4623c88c1f53d8d2394c6199f2921dea325"
|
||||
checksum = "01cec0252c2afff729ee6f00e903d479fba81784c8e2bd77447673471fdfaea1"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"parking",
|
||||
@@ -1807,7 +1813,7 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d96b852f1345da36d551b9473fa1e2b1eb5c5195585c6c018118bc92a8d91160"
|
||||
dependencies = [
|
||||
"event-listener 3.0.0",
|
||||
"event-listener 3.0.1",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
@@ -1864,9 +1870,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10"
|
||||
checksum = "64d6dafc854908ff5da46ff3f8f473c6984119a2876a383a860246dd7841a868"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
@@ -1893,9 +1899,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fiat-crypto"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0870c84016d4b481be5c9f323c24f65e31e901ae618f0e80f4308fb00de1d2d"
|
||||
checksum = "a481586acf778f1b1455424c343f71124b048ffa5f4fc3f8f6ae9dc432dcb3c7"
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
@@ -1968,9 +1974,9 @@ version = "1.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.28"
|
||||
version = "0.3.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
|
||||
checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@@ -1983,9 +1989,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.28"
|
||||
version = "0.3.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
|
||||
checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
@@ -1993,15 +1999,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.28"
|
||||
version = "0.3.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
|
||||
checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.28"
|
||||
version = "0.3.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
|
||||
checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
@@ -2010,15 +2016,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.28"
|
||||
version = "0.3.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
|
||||
checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
version = "2.0.0"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c1155db57329dca6d018b61e76b1488ce9a2e5e44028cac420a5898f4fcef63"
|
||||
checksum = "d3831c2651acb5177cbd83943f3d9c8912c5ad03c76afcc0e9511ba568ec5ebb"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"futures-core",
|
||||
@@ -2026,37 +2032,36 @@ dependencies = [
|
||||
"memchr",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
"waker-fn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.28"
|
||||
version = "0.3.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
|
||||
checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.38",
|
||||
"syn 2.0.39",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.28"
|
||||
version = "0.3.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
|
||||
checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.28"
|
||||
version = "0.3.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
|
||||
checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.28"
|
||||
version = "0.3.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
|
||||
checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@@ -2330,9 +2335,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "human-panic"
|
||||
version = "1.2.1"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b82da652938b83f94cfdaaf9ae7aaadb8430d84b0dfda226998416318727eac2"
|
||||
checksum = "7a79a67745be0cb8dd2771f03b24c2f25df98d5471fe7a595d668cfa2e6f843d"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"os_info",
|
||||
@@ -2460,9 +2465,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "imap-proto"
|
||||
version = "0.16.2"
|
||||
version = "0.16.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f73b1b63179418b20aa81002d616c5f21b4ba257da9bca6989ea64dc573933e0"
|
||||
checksum = "305c25c6e69416059e3396c4a062b84dc7b0a782cd4c84d82bab268eb0421ec7"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
@@ -2479,9 +2484,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.0.2"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897"
|
||||
checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.14.2",
|
||||
@@ -2591,9 +2596,9 @@ checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.64"
|
||||
version = "0.3.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a"
|
||||
checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
@@ -2651,9 +2656,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.149"
|
||||
version = "0.2.150"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b"
|
||||
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
@@ -2661,6 +2666,17 @@ version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"libc",
|
||||
"redox_syscall 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.26.0"
|
||||
@@ -2974,7 +2990,7 @@ checksum = "cfb77679af88f8b125209d354a202862602672222e7f2313fdd6dc349bad4712"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.38",
|
||||
"syn 2.0.39",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3067,9 +3083,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.57"
|
||||
version = "0.10.59"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c"
|
||||
checksum = "7a257ad03cd8fb16ad4172fedf8094451e1af1c4b70097636ef2eac9a5f0cc33"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"cfg-if",
|
||||
@@ -3088,7 +3104,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.38",
|
||||
"syn 2.0.39",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3108,9 +3124,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.93"
|
||||
version = "0.9.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d"
|
||||
checksum = "40a4130519a360279579c2053038317e40eff64d13fd3f004f9e1b72b8a6aaf9"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -3206,13 +3222,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.8"
|
||||
version = "0.9.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"
|
||||
checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall 0.3.5",
|
||||
"redox_syscall 0.4.1",
|
||||
"smallvec",
|
||||
"windows-targets",
|
||||
]
|
||||
@@ -3296,7 +3312,7 @@ dependencies = [
|
||||
"p384 0.13.0",
|
||||
"rand 0.8.5",
|
||||
"ripemd",
|
||||
"rsa 0.9.2",
|
||||
"rsa 0.9.3",
|
||||
"sha1",
|
||||
"sha2 0.10.8",
|
||||
"sha3",
|
||||
@@ -3325,7 +3341,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.38",
|
||||
"syn 2.0.39",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3391,9 +3407,9 @@ checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
|
||||
|
||||
[[package]]
|
||||
name = "platforms"
|
||||
version = "3.1.2"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4503fa043bf02cee09a9582e9554b4c6403b2ef55e4612e96561d294419429f8"
|
||||
checksum = "14e6ab3f592e6fb464fc9712d8d6e6912de6473954635fd76a589d832cffcbb0"
|
||||
|
||||
[[package]]
|
||||
name = "plotters"
|
||||
@@ -3444,12 +3460,13 @@ checksum = "3bccab0e7fd7cc19f820a1c8c91720af652d0c88dc9664dd72aef2614f04af3b"
|
||||
|
||||
[[package]]
|
||||
name = "postcard"
|
||||
version = "1.0.6"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9ee729232311d3cd113749948b689627618133b1c5012b77342c1950b25eaeb"
|
||||
checksum = "a55c51ee6c0db07e68448e336cf8ea4131a620edefebf9893e759b2d793420f8"
|
||||
dependencies = [
|
||||
"cobs",
|
||||
"const_format",
|
||||
"embedded-io",
|
||||
"postcard-derive",
|
||||
"serde",
|
||||
]
|
||||
@@ -3499,9 +3516,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "primeorder"
|
||||
version = "0.13.2"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c2fcef82c0ec6eefcc179b978446c399b3cdf73c392c35604e399eee6df1ee3"
|
||||
checksum = "c7dbe9ed3b56368bd99483eb32fe9c17fdd3730aebadc906918ce78d54c7eeb4"
|
||||
dependencies = [
|
||||
"elliptic-curve 0.13.6",
|
||||
]
|
||||
@@ -3784,15 +3801,6 @@ dependencies = [
|
||||
"yasna",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.3.5"
|
||||
@@ -3803,13 +3811,22 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.3"
|
||||
name = "redox_syscall"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
|
||||
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4"
|
||||
dependencies = [
|
||||
"getrandom 0.2.10",
|
||||
"redox_syscall 0.2.16",
|
||||
"libredox",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
@@ -3993,16 +4010,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "0.9.2"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8"
|
||||
checksum = "86ef35bf3e7fe15a53c4ab08a998e42271eab13eb0db224126bc7bc4c4bad96d"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"const-oid",
|
||||
"digest 0.10.7",
|
||||
"num-bigint-dig",
|
||||
"num-integer",
|
||||
"num-iter",
|
||||
"num-traits",
|
||||
"pkcs1 0.7.5",
|
||||
"pkcs8 0.10.2",
|
||||
@@ -4065,9 +4080,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.20"
|
||||
version = "0.38.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67ce50cb2e16c2903e30d1cbccfd8387a74b9d4c938b6a4c5ec6cc7556f7a8a0"
|
||||
checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"errno",
|
||||
@@ -4295,9 +4310,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.190"
|
||||
version = "1.0.191"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7"
|
||||
checksum = "a834c4821019838224821468552240d4d95d14e751986442c816572d39a080c9"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@@ -4322,13 +4337,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.190"
|
||||
version = "1.0.191"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3"
|
||||
checksum = "46fa52d5646bce91b680189fe5b1c049d2ea38dabb4e2e7c8d00ca12cfbfbcfd"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.38",
|
||||
"syn 2.0.39",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4344,9 +4359,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.107"
|
||||
version = "1.0.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65"
|
||||
checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
@@ -4629,7 +4644,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.38",
|
||||
"syn 2.0.39",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4651,9 +4666,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.38"
|
||||
version = "2.0.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b"
|
||||
checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4721,13 +4736,13 @@ checksum = "094c9f64d6de9a8506b1e49b63a29333b37ed9e821ee04be694d431b3264c3c5"
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.8.0"
|
||||
version = "3.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef"
|
||||
checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
"redox_syscall 0.3.5",
|
||||
"redox_syscall 0.4.1",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
]
|
||||
@@ -4783,7 +4798,7 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.38",
|
||||
"syn 2.0.39",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4898,7 +4913,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.38",
|
||||
"syn 2.0.39",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4980,9 +4995,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.7.8"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257"
|
||||
checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
@@ -5001,11 +5016,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.19.15"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
|
||||
checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03"
|
||||
dependencies = [
|
||||
"indexmap 2.0.2",
|
||||
"indexmap 2.1.0",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
@@ -5060,7 +5075,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.38",
|
||||
"syn 2.0.39",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5290,12 +5305,6 @@ version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "waker-fn"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.4.0"
|
||||
@@ -5335,9 +5344,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.87"
|
||||
version = "0.2.88"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342"
|
||||
checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen-macro",
|
||||
@@ -5345,24 +5354,24 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.87"
|
||||
version = "0.2.88"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd"
|
||||
checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.38",
|
||||
"syn 2.0.39",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.37"
|
||||
version = "0.4.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03"
|
||||
checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
@@ -5372,9 +5381,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.87"
|
||||
version = "0.2.88"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d"
|
||||
checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -5382,28 +5391,28 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.87"
|
||||
version = "0.2.88"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
|
||||
checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.38",
|
||||
"syn 2.0.39",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.87"
|
||||
version = "0.2.88"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
|
||||
checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b"
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.64"
|
||||
version = "0.3.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b"
|
||||
checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
@@ -5582,9 +5591,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.17"
|
||||
version = "0.5.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3b801d0e0a6726477cc207f60162da452f3a95adb368399bef20a946e06f65c"
|
||||
checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -5691,22 +5700,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.15"
|
||||
version = "0.7.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81ba595b9f2772fbee2312de30eeb80ec773b4cb2f1e8098db024afadda6c06f"
|
||||
checksum = "8cd369a67c0edfef15010f980c3cbe45d7f651deac2cd67ce097cd801de16557"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.15"
|
||||
version = "0.7.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "772666c41fb6dceaf520b564b962d738a8e1a83b41bd48945f50837aed78bb1d"
|
||||
checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.38",
|
||||
"syn 2.0.39",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5726,5 +5735,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.38",
|
||||
"syn 2.0.39",
|
||||
]
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.127.2"
|
||||
version = "1.129.1"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.70"
|
||||
@@ -90,7 +90,7 @@ tokio-io-timeout = "1.2.0"
|
||||
tokio-stream = { version = "0.1.14", features = ["fs"] }
|
||||
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
||||
tokio-util = "0.7.9"
|
||||
toml = "0.7"
|
||||
toml = "0.8"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,68 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
inkscape:version="1.0beta1 (32d4812, 2019-09-19)"
|
||||
sodipodi:docname="icon-group.svg"
|
||||
id="svg835"
|
||||
version="1.1"
|
||||
viewBox="0 0 60 60"
|
||||
height="60"
|
||||
width="60"
|
||||
inkscape:export-filename="/Users/bpetersen/projects/deltachat-core-rust/assets/icon-group.png"
|
||||
inkscape:export-xdpi="409.60001"
|
||||
inkscape:export-ydpi="409.60001">
|
||||
<metadata
|
||||
id="metadata841">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs839" />
|
||||
<sodipodi:namedview
|
||||
inkscape:current-layer="svg835"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:window-y="23"
|
||||
inkscape:window-x="46"
|
||||
inkscape:cy="32.923751"
|
||||
inkscape:cx="17.224772"
|
||||
inkscape:zoom="9.9662545"
|
||||
showgrid="false"
|
||||
id="namedview837"
|
||||
inkscape:window-height="1035"
|
||||
inkscape:window-width="1679"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0"
|
||||
guidetolerance="10"
|
||||
gridtolerance="10"
|
||||
objecttolerance="10"
|
||||
borderopacity="1"
|
||||
inkscape:document-rotation="0"
|
||||
bordercolor="#666666"
|
||||
pagecolor="#ffffff" />
|
||||
<rect
|
||||
style="fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:0.738492;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="rect838"
|
||||
width="60"
|
||||
height="60"
|
||||
x="0"
|
||||
y="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-width:1.74233"
|
||||
id="path833"
|
||||
fill="none"
|
||||
d="m 36.969303,28.257673 c 2.892261,0 5.209554,-2.334717 5.209554,-5.226978 0,-2.892262 -2.317293,-5.226978 -5.209554,-5.226978 -2.892261,0 -5.226978,2.334716 -5.226978,5.226978 0,2.892261 2.334717,5.226978 5.226978,5.226978 z m -13.938606,0 c 2.892261,0 5.209555,-2.334717 5.209555,-5.226978 0,-2.892262 -2.317294,-5.226978 -5.209555,-5.226978 -2.892261,0 -5.226978,2.334716 -5.226978,5.226978 0,2.892261 2.334717,5.226978 5.226978,5.226978 z m 0,3.484652 c -4.05962,0 -12.196282,2.038521 -12.196282,6.09814 v 4.355815 h 24.392562 v -4.355815 c 0,-4.059619 -8.13666,-6.09814 -12.19628,-6.09814 z m 13.938606,0 c -0.505274,0 -1.080241,0.03485 -1.690055,0.08712 2.021098,1.463552 3.432381,3.432381 3.432381,6.011023 v 4.355815 h 10.453956 v -4.355815 c 0,-4.059619 -8.136663,-6.09814 -12.196282,-6.09814 z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.8 KiB |
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.127.2"
|
||||
version = "1.129.1"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
+54
-22
@@ -2941,7 +2941,6 @@ int dc_receive_backup (dc_context_t* context, const char* qr);
|
||||
* use dc_accounts_remove_account().
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param os_name
|
||||
* @param dir The directory to create the context-databases in.
|
||||
* If the directory does not exist,
|
||||
* dc_accounts_new() will try to create it.
|
||||
@@ -3732,8 +3731,21 @@ int dc_chat_can_send (const dc_chat_t* chat);
|
||||
|
||||
/**
|
||||
* Check if a chat is protected.
|
||||
* Protected chats contain only verified members and encryption is always enabled.
|
||||
* Protected chats are created using dc_create_group_chat() by setting the 'protect' parameter to 1.
|
||||
*
|
||||
* End-to-end encryption is guaranteed in protected chats
|
||||
* and only verified contacts
|
||||
* as determined by dc_contact_is_verified()
|
||||
* can be added to protected chats.
|
||||
*
|
||||
* Protected chats are created using dc_create_group_chat()
|
||||
* by setting the 'protect' parameter to 1.
|
||||
* 1:1 chats become protected or unprotected automatically
|
||||
* if `verified_one_on_one_chats` setting is enabled.
|
||||
*
|
||||
* UI should display a green checkmark
|
||||
* in the chat title and
|
||||
* in the chatlist item
|
||||
* if chat protection is enabled.
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
* @param chat The chat object.
|
||||
@@ -5033,10 +5045,19 @@ int dc_contact_is_blocked (const dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
* Check if a contact was verified. E.g. by a secure-join QR code scan
|
||||
* and if the key has not changed since this verification.
|
||||
* Check if the contact
|
||||
* can be added to verified chats,
|
||||
* i.e. has a verified key
|
||||
* and Autocrypt key matches the verified key.
|
||||
*
|
||||
* The UI may draw a checkbox or something like that beside verified contacts.
|
||||
* If contact is verified
|
||||
* UI should display green checkmark after the contact name
|
||||
* in contact list items and
|
||||
* in chat member list items.
|
||||
*
|
||||
* Do not use this function when displaying the contact profile view.
|
||||
* Display green checkmark in the title of the contact profile
|
||||
* if dc_contact_profile_is_verified() returns true.
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
@@ -5046,32 +5067,37 @@ int dc_contact_is_blocked (const dc_contact_t* contact);
|
||||
int dc_contact_is_verified (dc_contact_t* contact);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Return the address that verified a contact
|
||||
* Check if the contact profile title
|
||||
* should contain a green checkmark.
|
||||
* This indicates whether 1:1 chat with
|
||||
* this contact has a green checkmark
|
||||
* or will have if such chat is created.
|
||||
*
|
||||
* The UI may use this in addition to a checkmark showing the verification status.
|
||||
* In case of verification chains,
|
||||
* the last contact in the chain is shown.
|
||||
* This is because of privacy reasons, but also as it would not help the user
|
||||
* to see a unknown name here - where one can mostly always ask the shown name
|
||||
* as it is directly known.
|
||||
* Use dc_contact_get_verified_id to display the verifier contact
|
||||
* in the info section of the contact profile.
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
* @return
|
||||
* 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
|
||||
* @return 1=contact profile has a green checkmark in the title,
|
||||
* 0=contact profile has no green checkmark in the title.
|
||||
*/
|
||||
char* dc_contact_get_verifier_addr (dc_contact_t* contact);
|
||||
int dc_contact_profile_is_verified (dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
* Return the `ContactId` that verified a contact
|
||||
* Return the contact ID that verified a contact.
|
||||
*
|
||||
* The UI may use this in addition to a checkmark showing the verification status
|
||||
* If the function returns non-zero result,
|
||||
* display green checkmark in the profile and "Introduced by ..." line
|
||||
* with the name and address of the contact
|
||||
* formatted by dc_contact_get_name_n_addr.
|
||||
*
|
||||
* If this function returns a verifier,
|
||||
* this does not necessarily mean
|
||||
* you can add the contact to verified chats.
|
||||
* Use dc_contact_is_verified() to check
|
||||
* if a contact can be added to a verified chat instead.
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
@@ -6234,6 +6260,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
* @param data2 (int) The progress as:
|
||||
* 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
||||
* (Bob has verified alice and waits until Alice does the same for him)
|
||||
* 1000=vg-member-added/vc-contact-confirm received
|
||||
*/
|
||||
#define DC_EVENT_SECUREJOIN_JOINER_PROGRESS 2061
|
||||
|
||||
@@ -7260,6 +7287,11 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used in info messages.
|
||||
#define DC_STR_CHAT_PROTECTION_DISABLED 171
|
||||
|
||||
/// "Others will only see this group after you sent a first message."
|
||||
///
|
||||
/// Used as the first info messages in newly created groups.
|
||||
#define DC_STR_NEW_GROUP_SEND_FIRST_MESSAGE 172
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
+10
-12
@@ -489,7 +489,7 @@ pub unsafe extern "C" fn dc_start_io(context: *mut dc_context_t) {
|
||||
if context.is_null() {
|
||||
return;
|
||||
}
|
||||
let ctx = &*context;
|
||||
let ctx = &mut *context;
|
||||
|
||||
block_on(ctx.start_io())
|
||||
}
|
||||
@@ -4120,20 +4120,18 @@ pub unsafe extern "C" fn dc_contact_is_verified(contact: *mut dc_contact_t) -> l
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_contact_get_verifier_addr(
|
||||
contact: *mut dc_contact_t,
|
||||
) -> *mut libc::c_char {
|
||||
pub unsafe extern "C" fn dc_contact_profile_is_verified(contact: *mut dc_contact_t) -> libc::c_int {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_get_verifier_addr()");
|
||||
return "".strdup();
|
||||
eprintln!("ignoring careless call to dc_contact_profile_is_verified()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_contact = &*contact;
|
||||
let ctx = &*ffi_contact.context;
|
||||
block_on(ffi_contact.contact.get_verifier_addr(ctx))
|
||||
.context("failed to get verifier for contact")
|
||||
|
||||
block_on(ffi_contact.contact.is_profile_verified(ctx))
|
||||
.context("is_profile_verified failed")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
.strdup()
|
||||
.unwrap_or_default() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -4946,8 +4944,8 @@ pub unsafe extern "C" fn dc_accounts_start_io(accounts: *mut dc_accounts_t) {
|
||||
return;
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
block_on(async move { accounts.read().await.start_io().await });
|
||||
let accounts = &mut *accounts;
|
||||
block_on(async move { accounts.write().await.start_io().await });
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.127.2"
|
||||
version = "1.129.1"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
|
||||
@@ -221,13 +221,13 @@ impl CommandApi {
|
||||
|
||||
/// Starts background tasks for all accounts.
|
||||
async fn start_io_for_all_accounts(&self) -> Result<()> {
|
||||
self.accounts.read().await.start_io().await;
|
||||
self.accounts.write().await.start_io().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stops background tasks for all accounts.
|
||||
async fn stop_io_for_all_accounts(&self) -> Result<()> {
|
||||
self.accounts.read().await.stop_io().await;
|
||||
self.accounts.write().await.stop_io().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -237,7 +237,7 @@ impl CommandApi {
|
||||
|
||||
/// Starts background tasks for a single account.
|
||||
async fn start_io(&self, account_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let mut ctx = self.get_context(account_id).await?;
|
||||
ctx.start_io().await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -383,7 +383,7 @@ impl CommandApi {
|
||||
/// Configures this account with the currently set parameters.
|
||||
/// Setup the credential config before calling this.
|
||||
async fn configure(&self, account_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let mut ctx = self.get_context(account_id).await?;
|
||||
ctx.stop_io().await;
|
||||
let result = ctx.configure().await;
|
||||
if result.is_err() {
|
||||
@@ -1391,6 +1391,19 @@ impl CommandApi {
|
||||
// chat
|
||||
// ---------------------------------------------
|
||||
|
||||
/// Returns the [`ChatId`] for the 1:1 chat with `contact_id` if it exists.
|
||||
///
|
||||
/// If it does not exist, `None` is returned.
|
||||
async fn get_chat_id_by_contact_id(
|
||||
&self,
|
||||
account_id: u32,
|
||||
contact_id: u32,
|
||||
) -> Result<Option<u32>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let chat_id = ChatId::lookup_by_contact(&ctx, ContactId::new(contact_id)).await?;
|
||||
Ok(chat_id.map(|id| id.to_u32()))
|
||||
}
|
||||
|
||||
/// Returns all message IDs of the given types in a chat.
|
||||
/// Typically used to show a gallery.
|
||||
///
|
||||
|
||||
@@ -18,6 +18,17 @@ use super::contact::ContactObject;
|
||||
pub struct FullChat {
|
||||
id: u32,
|
||||
name: String,
|
||||
|
||||
/// True if the chat is protected.
|
||||
///
|
||||
/// UI should display a green checkmark
|
||||
/// in the chat title,
|
||||
/// in the chat profile title and
|
||||
/// in the chatlist item
|
||||
/// if chat protection is enabled.
|
||||
/// UI should also display a green checkmark
|
||||
/// in the contact profile
|
||||
/// if 1:1 chat with this contact exists and is protected.
|
||||
is_protected: bool,
|
||||
profile_image: Option<String>, //BLOBS ?
|
||||
archived: bool,
|
||||
@@ -128,6 +139,17 @@ impl FullChat {
|
||||
pub struct BasicChat {
|
||||
id: u32,
|
||||
name: String,
|
||||
|
||||
/// True if the chat is protected.
|
||||
///
|
||||
/// UI should display a green checkmark
|
||||
/// in the chat title,
|
||||
/// in the chat profile title and
|
||||
/// in the chatlist item
|
||||
/// if chat protection is enabled.
|
||||
/// UI should also display a green checkmark
|
||||
/// in the contact profile
|
||||
/// if 1:1 chat with this contact exists and is protected.
|
||||
is_protected: bool,
|
||||
profile_image: Option<String>, //BLOBS ?
|
||||
archived: bool,
|
||||
|
||||
@@ -19,11 +19,29 @@ pub struct ContactObject {
|
||||
profile_image: Option<String>, // BLOBS
|
||||
name_and_addr: String,
|
||||
is_blocked: bool,
|
||||
|
||||
/// True if the contact can be added to verified groups.
|
||||
///
|
||||
/// If this is true
|
||||
/// UI should display green checkmark after the contact name
|
||||
/// in contact list items and
|
||||
/// in chat member list items.
|
||||
is_verified: bool,
|
||||
/// the address that verified this contact
|
||||
verifier_addr: Option<String>,
|
||||
/// the id of the contact that verified this contact
|
||||
|
||||
/// True if the contact profile title should have a green checkmark.
|
||||
///
|
||||
/// This indicates whether 1:1 chat has a green checkmark
|
||||
/// or will have a green checkmark if created.
|
||||
is_profile_verified: bool,
|
||||
|
||||
/// The ID of the contact that verified this contact.
|
||||
///
|
||||
/// If this is present,
|
||||
/// display a green checkmark and "Introduced by ..."
|
||||
/// string followed by the verifier contact name and address
|
||||
/// in the contact profile.
|
||||
verifier_id: Option<u32>,
|
||||
|
||||
/// the contact's last seen timestamp
|
||||
last_seen: i64,
|
||||
was_seen_recently: bool,
|
||||
@@ -39,18 +57,12 @@ impl ContactObject {
|
||||
None => None,
|
||||
};
|
||||
let is_verified = contact.is_verified(context).await? == VerifiedStatus::BidirectVerified;
|
||||
let is_profile_verified = contact.is_profile_verified(context).await?;
|
||||
|
||||
let (verifier_addr, verifier_id) = if is_verified {
|
||||
(
|
||||
contact.get_verifier_addr(context).await?,
|
||||
contact
|
||||
.get_verifier_id(context)
|
||||
.await?
|
||||
.map(|contact_id| contact_id.to_u32()),
|
||||
)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
let verifier_id = contact
|
||||
.get_verifier_id(context)
|
||||
.await?
|
||||
.map(|contact_id| contact_id.to_u32());
|
||||
|
||||
Ok(ContactObject {
|
||||
address: contact.get_addr().to_owned(),
|
||||
@@ -64,7 +76,7 @@ impl ContactObject {
|
||||
name_and_addr: contact.get_name_n_addr(),
|
||||
is_blocked: contact.is_blocked(),
|
||||
is_verified,
|
||||
verifier_addr,
|
||||
is_profile_verified,
|
||||
verifier_id,
|
||||
last_seen: contact.last_seen(),
|
||||
was_seen_recently: contact.was_seen_recently(),
|
||||
|
||||
@@ -28,7 +28,7 @@ async fn main() -> Result<(), std::io::Error> {
|
||||
.layer(Extension(state.clone()));
|
||||
|
||||
tokio::spawn(async move {
|
||||
state.accounts.read().await.start_io().await;
|
||||
state.accounts.write().await.start_io().await;
|
||||
});
|
||||
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], port));
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"@types/chai": "^4.2.21",
|
||||
"@types/chai-as-promised": "^7.1.5",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/ws": "^7.2.4",
|
||||
"c8": "^7.10.0",
|
||||
"chai": "^4.3.4",
|
||||
@@ -17,7 +16,6 @@
|
||||
"esbuild": "^0.17.9",
|
||||
"http-server": "^14.1.1",
|
||||
"mocha": "^9.1.1",
|
||||
"node-fetch": "^2.6.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.6.2",
|
||||
"typedoc": "^0.23.2",
|
||||
@@ -55,5 +53,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.127.2"
|
||||
"version": "1.129.1"
|
||||
}
|
||||
|
||||
@@ -79,6 +79,9 @@ describe("basic tests", () => {
|
||||
accountId = await dc.rpc.addAccount();
|
||||
});
|
||||
it("should block and unblock contact", async function () {
|
||||
// Cannot send sync messages to self as we do not have a self address.
|
||||
await dc.rpc.setConfig(accountId, "sync_msgs", "0");
|
||||
|
||||
const contactId = await dc.rpc.createContact(
|
||||
accountId,
|
||||
"example@delta.chat",
|
||||
|
||||
@@ -2,7 +2,6 @@ import { tmpdir } from "os";
|
||||
import { join, resolve } from "path";
|
||||
import { mkdtemp, rm } from "fs/promises";
|
||||
import { spawn, exec } from "child_process";
|
||||
import fetch from "node-fetch";
|
||||
import { Readable, Writable } from "node:stream";
|
||||
|
||||
export type RpcServerHandle = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.127.2"
|
||||
version = "1.129.1"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
|
||||
|
||||
@@ -401,7 +401,7 @@ enum ExitResult {
|
||||
|
||||
async fn handle_cmd(
|
||||
line: &str,
|
||||
ctx: Context,
|
||||
mut ctx: Context,
|
||||
selected_chat: &mut ChatId,
|
||||
) -> Result<ExitResult, Error> {
|
||||
let mut args = line.splitn(2, ' ');
|
||||
|
||||
@@ -4,7 +4,7 @@ from warnings import warn
|
||||
|
||||
from ._utils import AttrDict
|
||||
from .chat import Chat
|
||||
from .const import ChatlistFlag, ContactFlag, SpecialContactId
|
||||
from .const import ChatlistFlag, ContactFlag, EventType, SpecialContactId
|
||||
from .contact import Contact
|
||||
from .message import Message
|
||||
|
||||
@@ -250,6 +250,13 @@ class Account:
|
||||
next_msg_ids = self._rpc.wait_next_msgs(self.id)
|
||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||
|
||||
def wait_for_incoming_msg_event(self):
|
||||
"""Wait for incoming message event and return it."""
|
||||
while True:
|
||||
event = self.wait_for_event()
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
return event
|
||||
|
||||
def get_fresh_messages_in_arrival_order(self) -> List[Message]:
|
||||
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
|
||||
warn(
|
||||
|
||||
@@ -21,7 +21,9 @@ class ACFactory:
|
||||
self.deltachat = deltachat
|
||||
|
||||
def get_unconfigured_account(self) -> Account:
|
||||
return self.deltachat.add_account()
|
||||
account = self.deltachat.add_account()
|
||||
account.set_config("verified_one_on_one_chats", "1")
|
||||
return account
|
||||
|
||||
def get_unconfigured_bot(self) -> Bot:
|
||||
return Bot(self.get_unconfigured_account())
|
||||
@@ -59,6 +61,16 @@ class ACFactory:
|
||||
def get_online_accounts(self, num: int) -> List[Account]:
|
||||
return [self.get_online_account() for _ in range(num)]
|
||||
|
||||
def resetup_account(self, ac: Account) -> Account:
|
||||
"""Resetup account from scratch, losing the encryption key."""
|
||||
ac.stop_io()
|
||||
ac_clone = self.get_unconfigured_account()
|
||||
for i in ["addr", "mail_pw"]:
|
||||
ac_clone.set_config(i, ac.get_config(i))
|
||||
ac.remove()
|
||||
ac_clone.configure()
|
||||
return ac_clone
|
||||
|
||||
def send_message(
|
||||
self,
|
||||
to_account: Account,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -5,7 +6,7 @@ import subprocess
|
||||
import sys
|
||||
from queue import Queue
|
||||
from threading import Event, Thread
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any, Dict, Iterator, Optional
|
||||
|
||||
|
||||
class JsonRpcError(Exception):
|
||||
@@ -23,7 +24,7 @@ class Rpc:
|
||||
|
||||
self._kwargs = kwargs
|
||||
self.process: subprocess.Popen
|
||||
self.id: int
|
||||
self.id_iterator: Iterator[int]
|
||||
self.event_queues: Dict[int, Queue]
|
||||
# Map from request ID to `threading.Event`.
|
||||
self.request_events: Dict[int, Event]
|
||||
@@ -54,7 +55,7 @@ class Rpc:
|
||||
preexec_fn=os.setpgrp, # noqa: PLW1509
|
||||
**self._kwargs,
|
||||
)
|
||||
self.id = 0
|
||||
self.id_iterator = itertools.count(start=1)
|
||||
self.event_queues = {}
|
||||
self.request_events = {}
|
||||
self.request_results = {}
|
||||
@@ -131,7 +132,9 @@ class Rpc:
|
||||
event = self.get_next_event()
|
||||
account_id = event["contextId"]
|
||||
queue = self.get_queue(account_id)
|
||||
queue.put(event["event"])
|
||||
event = event["event"]
|
||||
logging.debug("account_id=%d got an event %s", account_id, event)
|
||||
queue.put(event)
|
||||
except Exception:
|
||||
# Log an exception if the event loop dies.
|
||||
logging.exception("Exception in the event loop")
|
||||
@@ -143,14 +146,12 @@ class Rpc:
|
||||
|
||||
def __getattr__(self, attr: str):
|
||||
def method(*args) -> Any:
|
||||
self.id += 1
|
||||
request_id = self.id
|
||||
|
||||
request_id = next(self.id_iterator)
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": attr,
|
||||
"params": args,
|
||||
"id": self.id,
|
||||
"id": request_id,
|
||||
}
|
||||
event = Event()
|
||||
self.request_events[request_id] = event
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
from deltachat_rpc_client import Chat, SpecialContactId
|
||||
|
||||
|
||||
def test_qr_setup_contact(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
qr_code, _svg = alice.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
|
||||
while True:
|
||||
event = alice.wait_for_event()
|
||||
if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
# Test that Alice verified Bob's profile.
|
||||
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
|
||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||
assert alice_contact_bob_snapshot.is_verified
|
||||
assert alice_contact_bob_snapshot.is_profile_verified
|
||||
|
||||
while True:
|
||||
event = bob.wait_for_event()
|
||||
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
# Test that Bob verified Alice's profile.
|
||||
bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
|
||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||
assert bob_contact_alice_snapshot.is_verified
|
||||
assert bob_contact_alice_snapshot.is_profile_verified
|
||||
|
||||
|
||||
def test_qr_securejoin(acfactory):
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
logging.info("Alice creates a verified group")
|
||||
alice_chat = alice.create_group("Verified group", protect=True)
|
||||
|
||||
logging.info("Bob joins verified group")
|
||||
qr_code, _svg = alice_chat.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
while True:
|
||||
event = alice.wait_for_event()
|
||||
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
# Test that Alice verified Bob's profile.
|
||||
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
|
||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||
assert alice_contact_bob_snapshot.is_verified
|
||||
assert alice_contact_bob_snapshot.is_profile_verified
|
||||
|
||||
while True:
|
||||
event = bob.wait_for_event()
|
||||
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
# Test that Bob verified Alice's profile.
|
||||
bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
|
||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||
assert bob_contact_alice_snapshot.is_verified
|
||||
assert bob_contact_alice_snapshot.is_profile_verified
|
||||
|
||||
|
||||
@pytest.mark.xfail()
|
||||
def test_verified_group_recovery(acfactory, rpc) -> None:
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
|
||||
logging.info("ac1 creates verified group")
|
||||
chat = ac1.create_group("Verified group", protect=True)
|
||||
assert chat.get_basic_snapshot().is_protected
|
||||
|
||||
logging.info("ac2 joins verified group")
|
||||
qr_code, _svg = chat.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
while True:
|
||||
event = ac1.wait_for_event()
|
||||
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
# ac1 has ac2 directly verified.
|
||||
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
|
||||
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
|
||||
|
||||
logging.info("ac3 joins verified group")
|
||||
ac3_chat = ac3.secure_join(qr_code)
|
||||
while True:
|
||||
event = ac1.wait_for_event()
|
||||
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
logging.info("ac2 logs in on a new device")
|
||||
ac2 = acfactory.resetup_account(ac2)
|
||||
|
||||
logging.info("ac2 reverifies with ac3")
|
||||
qr_code, _svg = ac3.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
|
||||
while True:
|
||||
event = ac3.wait_for_event()
|
||||
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
logging.info("ac3 sends a message to the group")
|
||||
assert len(ac3_chat.get_contacts()) == 3
|
||||
ac3_chat.send_text("Hi!")
|
||||
|
||||
msg_id = ac2.wait_for_incoming_msg_event().msg_id
|
||||
message = ac2.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
logging.info("Received message %s", snapshot.text)
|
||||
assert snapshot.text == "Hi!"
|
||||
|
||||
# ac1 contact cannot be verified by ac2 because ac3 did not gossip ac1 key in the "Hi!" message.
|
||||
ac1_contact = ac2.get_contact_by_addr(ac1.get_config("addr"))
|
||||
assert not ac1_contact.get_snapshot().is_verified
|
||||
|
||||
ac3_contact_id_ac1 = rpc.lookup_contact_id_by_addr(ac3.id, ac1.get_config("addr"))
|
||||
ac3_chat.remove_contact(ac3_contact_id_ac1)
|
||||
ac3_chat.add_contact(ac3_contact_id_ac1)
|
||||
|
||||
msg_id = ac2.wait_for_incoming_msg_event().msg_id
|
||||
message = ac2.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
logging.info("ac2 got event message: %s", snapshot.text)
|
||||
assert "removed" in snapshot.text
|
||||
|
||||
event = ac2.wait_for_incoming_msg_event()
|
||||
msg_id = event.msg_id
|
||||
chat_id = event.chat_id
|
||||
message = ac2.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
logging.info("ac2 got event message: %s", snapshot.text)
|
||||
assert "added" in snapshot.text
|
||||
|
||||
assert ac1_contact.get_snapshot().is_verified
|
||||
|
||||
chat = Chat(ac2, chat_id)
|
||||
chat.send_text("Works again!")
|
||||
|
||||
msg_id = ac3.wait_for_incoming_msg_event().msg_id
|
||||
message = ac3.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.text == "Works again!"
|
||||
|
||||
ac1.wait_for_incoming_msg_event() # Hi!
|
||||
ac1.wait_for_incoming_msg_event() # Member removed
|
||||
ac1.wait_for_incoming_msg_event() # Member added
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Works again!"
|
||||
|
||||
# ac2 is now verified by ac3 for ac1
|
||||
ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr"))
|
||||
assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id
|
||||
|
||||
|
||||
def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
|
||||
logging.info("ac1 creates verified group")
|
||||
chat = ac1.create_group("Verified group", protect=True)
|
||||
assert chat.get_basic_snapshot().is_protected
|
||||
|
||||
logging.info("ac2 joins verified group")
|
||||
qr_code, _svg = chat.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
while True:
|
||||
event = ac1.wait_for_event()
|
||||
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
# ac1 has ac2 directly verified.
|
||||
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
|
||||
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
|
||||
|
||||
logging.info("ac3 joins verified group")
|
||||
ac3_chat = ac3.secure_join(qr_code)
|
||||
while True:
|
||||
event = ac1.wait_for_event()
|
||||
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
logging.info("ac2 logs in on a new device")
|
||||
ac2 = acfactory.resetup_account(ac2)
|
||||
|
||||
logging.info("ac2 reverifies with ac3")
|
||||
qr_code, _svg = ac3.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
|
||||
while True:
|
||||
event = ac3.wait_for_event()
|
||||
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
logging.info("ac3 sends a message to the group")
|
||||
assert len(ac3_chat.get_contacts()) == 3
|
||||
ac3_chat.send_text("Hi!")
|
||||
|
||||
msg_id = ac2.wait_for_incoming_msg_event().msg_id
|
||||
message = ac2.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
logging.info("Received message %s", snapshot.text)
|
||||
assert snapshot.text == "Hi!"
|
||||
|
||||
ac1.wait_for_incoming_msg_event() # Hi!
|
||||
|
||||
ac3_contact_ac2 = ac3.get_contact_by_addr(ac2.get_config("addr"))
|
||||
ac3_chat.remove_contact(ac3_contact_ac2)
|
||||
ac3_chat.add_contact(ac3_contact_ac2)
|
||||
|
||||
msg_id = ac2.wait_for_incoming_msg_event().msg_id
|
||||
message = ac2.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
assert "removed" in snapshot.text
|
||||
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert "removed" in snapshot.text
|
||||
|
||||
event = ac2.wait_for_incoming_msg_event()
|
||||
msg_id = event.msg_id
|
||||
chat_id = event.chat_id
|
||||
message = ac2.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
logging.info("ac2 got event message: %s", snapshot.text)
|
||||
assert "added" in snapshot.text
|
||||
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert "added" in snapshot.text
|
||||
|
||||
logging.info("ac2 address is %s", ac2.get_config("addr"))
|
||||
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
|
||||
ac1_contact_ac2_snapshot = ac1_contact_ac2.get_snapshot()
|
||||
# assert ac1_contact_ac2_snapshot.is_verified
|
||||
assert ac1_contact_ac2_snapshot.verifier_id == ac1.get_contact_by_addr(ac3.get_config("addr")).id
|
||||
|
||||
chat = Chat(ac2, chat_id)
|
||||
chat.send_text("Works again!")
|
||||
|
||||
msg_id = ac3.wait_for_incoming_msg_event().msg_id
|
||||
message = ac3.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.text == "Works again!"
|
||||
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Works again!"
|
||||
|
||||
# ac2 is now verified by ac3 for ac1
|
||||
ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr"))
|
||||
assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id
|
||||
@@ -377,15 +377,3 @@ def test_provider_info(rpc) -> None:
|
||||
rpc.set_config(account_id, "socks5_enabled", "1")
|
||||
provider_info = rpc.get_provider_info(account_id, "github.com")
|
||||
assert provider_info is None
|
||||
|
||||
|
||||
def test_qr_setup_contact(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
qr_code, _svg = alice.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
|
||||
while True:
|
||||
event = alice.wait_for_event()
|
||||
if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||
return
|
||||
|
||||
@@ -30,4 +30,4 @@ commands =
|
||||
[pytest]
|
||||
timeout = 300
|
||||
log_cli = true
|
||||
log_level = info
|
||||
log_level = debug
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.127.2"
|
||||
version = "1.129.1"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -35,10 +35,10 @@ skip = [
|
||||
{ name = "rand_chacha", version = "<0.3" },
|
||||
{ name = "rand_core", version = "<0.6" },
|
||||
{ name = "rand", version = "<0.8" },
|
||||
{ name = "redox_syscall", version = "0.2.16" },
|
||||
{ name = "redox_syscall", version = "0.3.5" },
|
||||
{ name = "regex-automata", version = "0.1.10" },
|
||||
{ name = "ring", version = "0.16.20" },
|
||||
{ name = "regex-syntax", version = "0.6.29" },
|
||||
{ name = "ring", version = "0.16.20" },
|
||||
{ name = "sec1", version = "0.3.0" },
|
||||
{ name = "sha2", version = "<0.10" },
|
||||
{ name = "signature", version = "1.6.4" },
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
# New acpeerstates table
|
||||
|
||||
A proposal to replace `acpeerstates` table,
|
||||
which has columns
|
||||
- `id`
|
||||
- `addr`
|
||||
- `prefer_encrypted`
|
||||
- `last_seen`
|
||||
- `last_seen_autocrypt`
|
||||
- `public_key`
|
||||
- `public_key_fingrprint`
|
||||
- `gossip_timestamp`
|
||||
- `gossip_key`
|
||||
- `gossip_key_fingerprint`
|
||||
- `verified_key`
|
||||
- `verified_key_fingerprint`
|
||||
- `verifier`
|
||||
- `secondary_verified_key`
|
||||
- `secondary_verified_key_fingerprint`
|
||||
- `secondary_verifier`
|
||||
with a new `public_keys` table with columns
|
||||
- `id`
|
||||
- `addr` - address of the contact which is supposed to have the private key
|
||||
- `public_key` - a blob with the OpenPGP public key
|
||||
- `public_key_fingerprint` - public key fingerprint
|
||||
- `introduced_by` - address of the contact if received directly in `Autocrypt` header, address of `Autocrypt-Gossip` sender otherwise
|
||||
- `is_verified` - boolean flag indicating if the key was received in a verified group or not
|
||||
- `timestamp` - timestamp of the most recent row update
|
||||
|
||||
The table has consraints `UNIQUE(addr, introduced_by)`.
|
||||
Note that there may be multiple rows with the same fingerprint,
|
||||
e.g. if multiple peers gossiped the same key for a contact they all are introducers of the key.
|
||||
|
||||
`prefer_encrypted` is moved to the `contacts` table.
|
||||
|
||||
|
||||
## Using the table to select the keys
|
||||
|
||||
### Sending a message to 1:1 chat
|
||||
|
||||
When sending a message to the contact in a 1:1 chat,
|
||||
encrypt to the key where `introduced_by` equals `addr`:
|
||||
```
|
||||
SELECT *
|
||||
FROM public_keys
|
||||
WHERE addr=? AND introduced_by=addr
|
||||
```
|
||||
This row is guaranteed to be unique if it exists.
|
||||
|
||||
If direct Autocrypt key does not exist,
|
||||
use the most recent one according to the timestamp key gossiped for this contact:
|
||||
```
|
||||
SELECT *
|
||||
FROM public_keys
|
||||
WHERE addr=?
|
||||
ORDER BY timestamp DESC LIMIT 1
|
||||
```
|
||||
|
||||
If the table contains a row with selected key and `is_verified` flag set,
|
||||
display a green checkmark and "Introduced by <introduced_by>" in the 1:1 chat/contact profile.
|
||||
Note that row with verification may be older than the most recent gossip.
|
||||
|
||||
### Sending a message to unprotected group chat
|
||||
|
||||
When sending a message to the contact in a non-verified group chat,
|
||||
encrypt to the same key as used in 1:1 chat
|
||||
and (NEW!) one or more most recent gossiped keys introduced by chat members:
|
||||
```
|
||||
SELECT public_key
|
||||
FROM public_keys
|
||||
WHERE addr=? AND introduced_by IN (<chat-member-list>)
|
||||
ORDER BY timestamp DESC
|
||||
```
|
||||
It does not matter if the gossiped key has `is_verified` flag.
|
||||
|
||||
### Sending a message to protected group chat
|
||||
|
||||
When sending a message in a protected group chat,
|
||||
construct a list of candidate keys for each contact
|
||||
the same way as in an unprotected chat,
|
||||
but then filter out the keys which do not have `is_verified` flag.
|
||||
|
||||
|
||||
## Updating the table
|
||||
|
||||
When executing "setup contact" protocol
|
||||
with the contact,
|
||||
set the row with `addr` and `introduced_by` being equal
|
||||
to the contact address and `is_verified` is true.
|
||||
In this case the contact becomes directly verified.
|
||||
|
||||
When receiving a message,
|
||||
first process the `Autocrypt` key,
|
||||
then check verification properties to detect if gossip is signed with a verified key,
|
||||
then process `Autocrypt-Gossip` key.
|
||||
|
||||
### 1. Processing the Autocrypt header
|
||||
|
||||
Take the key from the Autocrypt header
|
||||
and insert or update the row
|
||||
identified by `addr` and `introduced_by` being equal to the `From:` address.
|
||||
Notify about setup change and reset `is_verified` flag if the key fingerprint has changed.
|
||||
Always update the timestamp.
|
||||
|
||||
Update `prefer_encrypted` in the contacts table as needed
|
||||
based on the `Autocrypt` header parameters,
|
||||
whether the message is signed etc.
|
||||
|
||||
### 2. Check verification
|
||||
|
||||
Determine whether the green checkmark should now be displayed
|
||||
on the 1:1 chat and update it accordingly,
|
||||
insert system messages,
|
||||
get it into broken state or out of it.
|
||||
|
||||
Now we know if the message is signed with a verified key.
|
||||
|
||||
### 3. Process `Autocrypt-Gossip`
|
||||
|
||||
Insert or update gossip keys
|
||||
```
|
||||
INSERT INTO public_keys (addr, public_key, public_key_fingerprint, introduced_by, is_verified, timestamp)
|
||||
VALUES (?, ?, ?, <introducer>, ?, <now>)
|
||||
ON CONFLICT
|
||||
DO UPDATE SET public_key=excluded.public_key, public_key_fingerprint=excluded.public_key_fingerprint
|
||||
```
|
||||
|
||||
`is_verified` should be set if the message is signed with a verified key,
|
||||
independently of whether the key was gossiped in a protected chat or not.
|
||||
+5
-5
@@ -40,7 +40,7 @@ npm install deltachat-node
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Nodejs >= `v16.0.0`
|
||||
- Nodejs >= `v18.0.0`
|
||||
- rustup (optional if you can't use the prebuilds)
|
||||
|
||||
> On Windows, you may need to also install **Perl** to be able to compile deltachat-core.
|
||||
@@ -113,8 +113,8 @@ Then, in the `deltachat-desktop` repository, run:
|
||||
deltachat doesn't support universal (fat) binaries (that contain builds for both cpu architectures) yet, until it does you can use the following workaround to get x86_64 builds:
|
||||
|
||||
```
|
||||
$ fnm install 17 --arch x64
|
||||
$ fnm use 17
|
||||
$ fnm install 19 --arch x64
|
||||
$ fnm use 19
|
||||
$ node -p process.arch
|
||||
# result should be x64
|
||||
$ rustup target add x86_64-apple-darwin
|
||||
@@ -127,8 +127,8 @@ $ npm run test
|
||||
If your node and electron are already build for arm64 you can also try building for arm:
|
||||
|
||||
```
|
||||
$ fnm install 16 --arch arm64
|
||||
$ fnm use 16
|
||||
$ fnm install 18 --arch arm64
|
||||
$ fnm use 18
|
||||
$ node -p process.arch
|
||||
# result should be arm64
|
||||
$ npm_config_arch=arm64 npm run build
|
||||
|
||||
@@ -239,6 +239,7 @@ module.exports = {
|
||||
DC_STR_MSGGRPNAME: 15,
|
||||
DC_STR_MSGLOCATIONDISABLED: 65,
|
||||
DC_STR_MSGLOCATIONENABLED: 64,
|
||||
DC_STR_NEW_GROUP_SEND_FIRST_MESSAGE: 172,
|
||||
DC_STR_NOMESSAGES: 1,
|
||||
DC_STR_NOT_CONNECTED: 121,
|
||||
DC_STR_NOT_SUPPORTED_BY_PROVIDER: 113,
|
||||
|
||||
@@ -239,6 +239,7 @@ export enum C {
|
||||
DC_STR_MSGGRPNAME = 15,
|
||||
DC_STR_MSGLOCATIONDISABLED = 65,
|
||||
DC_STR_MSGLOCATIONENABLED = 64,
|
||||
DC_STR_NEW_GROUP_SEND_FIRST_MESSAGE = 172,
|
||||
DC_STR_NOMESSAGES = 1,
|
||||
DC_STR_NOT_CONNECTED = 121,
|
||||
DC_STR_NOT_SUPPORTED_BY_PROVIDER = 113,
|
||||
|
||||
+8
-3
@@ -8,7 +8,6 @@ import { EventId2EventName, C } from '../dist/constants'
|
||||
import { join } from 'path'
|
||||
import { statSync } from 'fs'
|
||||
import { Context } from '../dist/context'
|
||||
import fetch from 'node-fetch'
|
||||
chai.use(chaiAsPromised)
|
||||
chai.config.truncateThreshold = 0 // Do not truncate assertion errors.
|
||||
|
||||
@@ -440,6 +439,12 @@ describe('Offline Tests with unconfigured account', function () {
|
||||
expect(blobPath.startsWith(blobs)).to.be.true
|
||||
expect(blobPath.includes('image')).to.be.true
|
||||
expect(blobPath.endsWith('.jpeg')).to.be.true
|
||||
|
||||
context.setChatProfileImage(chatId, null)
|
||||
expect(context.getChat(chatId).getProfileImage()).to.be.equal(
|
||||
null,
|
||||
'image is null'
|
||||
)
|
||||
})
|
||||
|
||||
it('test setting ephemeral timer', function () {
|
||||
@@ -662,9 +667,9 @@ describe('Offline Tests with unconfigured account', function () {
|
||||
|
||||
const lot = chatList.getSummary(0)
|
||||
strictEqual(lot.getId(), 0, 'lot has no id')
|
||||
strictEqual(lot.getState(), C.DC_STATE_UNDEFINED, 'correct state')
|
||||
strictEqual(lot.getState(), C.DC_STATE_IN_NOTICED, 'correct state')
|
||||
|
||||
const text = 'No messages.'
|
||||
const text = 'Others will only see this group after you sent a first message.'
|
||||
context.createGroupChat('groupchat1111')
|
||||
chatList = context.getChatList(0, 'groupchat1111', null)
|
||||
strictEqual(
|
||||
|
||||
+3
-3
@@ -13,10 +13,10 @@
|
||||
},
|
||||
"exclude": ["node_modules", "deltachat-core-rust", "dist", "scripts"],
|
||||
"typedocOptions": {
|
||||
"mode": "file",
|
||||
"out": "docs",
|
||||
"excludeNotExported": true,
|
||||
"excludePrivate": true,
|
||||
"defaultCategory": "index",
|
||||
"includeVersion": true
|
||||
"includeVersion": true,
|
||||
"entryPoints": ["lib/index.ts"]
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -6,7 +6,7 @@ E.g via <https://git-scm.com/download/win>
|
||||
|
||||
## install node
|
||||
|
||||
Download and install `v16` from <https://nodejs.org/en/>
|
||||
Download and install `v18` from <https://nodejs.org/en/>
|
||||
|
||||
## install rust
|
||||
|
||||
|
||||
+11
-15
@@ -2,28 +2,25 @@
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
"napi-macros": "^2.0.0",
|
||||
"node-gyp-build": "^4.1.0"
|
||||
"node-gyp-build": "^4.6.1"
|
||||
},
|
||||
"description": "node.js bindings for deltachat-core",
|
||||
"devDependencies": {
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/node": "^16.11.26",
|
||||
"@types/node": "^20.8.10",
|
||||
"chai": "^4.2.0",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"esm": "^3.2.25",
|
||||
"hallmark": "^2.0.0",
|
||||
"mocha": "^8.2.1",
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-gyp": "^9.0.0",
|
||||
"opn-cli": "^5.0.0",
|
||||
"prebuildify": "^3.0.0",
|
||||
"prebuildify-ci": "^1.0.4",
|
||||
"prettier": "^2.0.5",
|
||||
"typedoc": "^0.17.0",
|
||||
"typescript": "^3.9.10"
|
||||
"node-gyp": "^10.0.0",
|
||||
"prebuildify": "^5.0.1",
|
||||
"prebuildify-ci": "^1.0.5",
|
||||
"prettier": "^3.0.3",
|
||||
"typedoc": "^0.25.3",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"files": [
|
||||
"node/scripts/*",
|
||||
@@ -49,16 +46,15 @@
|
||||
"build:core:rust": "node node/scripts/rebuild-core.js",
|
||||
"clean": "rm -rf node/dist node/build node/prebuilds node/node_modules ./target",
|
||||
"download-prebuilds": "prebuildify-ci download",
|
||||
"hallmark": "hallmark --fix",
|
||||
"install": "node node/scripts/install.js",
|
||||
"install:prebuilds": "cd node && node-gyp-build \"npm run build:core\" \"npm run build:bindings:c:postinstall\"",
|
||||
"lint": "prettier --check \"node/lib/**/*.{ts,tsx}\"",
|
||||
"lint-fix": "prettier --write \"node/lib/**/*.{ts,tsx}\" \"node/test/**/*.js\"",
|
||||
"prebuildify": "cd node && prebuildify -t 16.13.0 --napi --strip --postinstall \"node scripts/postinstall.js --prebuild\"",
|
||||
"prebuildify": "cd node && prebuildify -t 18.0.0 --napi --strip --postinstall \"node scripts/postinstall.js --prebuild\"",
|
||||
"test": "npm run test:lint && npm run test:mocha",
|
||||
"test:lint": "npm run lint",
|
||||
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.127.2"
|
||||
"version": "1.129.1"
|
||||
}
|
||||
|
||||
@@ -571,11 +571,11 @@ class Account:
|
||||
return ScannedQRCode(lot)
|
||||
|
||||
def qr_setup_contact(self, qr):
|
||||
"""setup contact and return a Chat after contact is established.
|
||||
"""setup contact and return a `Chat` instance after contact is established.
|
||||
|
||||
This function triggers a network protocol in the background between
|
||||
the emitter of the QR code and this account.
|
||||
|
||||
Note that this function may block for a long time as messages are exchanged
|
||||
with the emitter of the QR code. On success a :class:`deltachat.chat.Chat` instance
|
||||
is returned.
|
||||
:param qr: valid "setup contact" QR code (all other QR codes will result in an exception)
|
||||
"""
|
||||
assert self.check_qr(qr).is_ask_verifycontact()
|
||||
@@ -585,11 +585,11 @@ class Account:
|
||||
return Chat(self, chat_id)
|
||||
|
||||
def qr_join_chat(self, qr):
|
||||
"""join a chat group through a QR code.
|
||||
"""return a `Chat` instance for which the securejoin network
|
||||
protocol has been started.
|
||||
|
||||
Note that this function may block for a long time as messages are exchanged
|
||||
with the emitter of the QR code. On success a :class:`deltachat.chat.Chat` instance
|
||||
is returned which is the chat that we just joined.
|
||||
This function triggers a network protocol in the background between
|
||||
the emitter of the QR code and this account.
|
||||
|
||||
:param qr: valid "join-group" QR code (all other QR codes will result in an exception)
|
||||
"""
|
||||
|
||||
@@ -546,6 +546,7 @@ class ACFactory:
|
||||
configdict.setdefault("bcc_self", False)
|
||||
configdict.setdefault("mvbox_move", False)
|
||||
configdict.setdefault("sentbox_watch", False)
|
||||
configdict.setdefault("sync_msgs", False)
|
||||
ac.update_config(configdict)
|
||||
self._preconfigure_key(ac, configdict["addr"])
|
||||
return ac
|
||||
|
||||
@@ -367,7 +367,7 @@ def test_webxdc_download_on_demand(acfactory, data, lp):
|
||||
|
||||
lp.sec("ac2 sets download limit")
|
||||
ac2.set_config("download_limit", "100")
|
||||
assert msg1.send_status_update({"payload": base64.b64encode(os.urandom(50000))}, "some test data")
|
||||
assert msg1.send_status_update({"payload": base64.b64encode(os.urandom(300000))}, "some test data")
|
||||
ac2_update = ac2._evtracker.wait_next_incoming_message()
|
||||
assert ac2_update.download_state == dc.const.DC_DOWNLOAD_AVAILABLE
|
||||
assert not msg2.get_status_updates()
|
||||
@@ -489,7 +489,7 @@ def test_forward_messages(acfactory, lp):
|
||||
lp.sec("ac2: check new chat has a forwarded message")
|
||||
assert chat3.is_promoted()
|
||||
messages = chat3.get_messages()
|
||||
assert len(messages) == 1
|
||||
assert len(messages) == 2
|
||||
msg = messages[-1]
|
||||
assert msg.is_forwarded()
|
||||
ac2.delete_messages(messages)
|
||||
@@ -1445,7 +1445,7 @@ def test_reaction_to_partially_fetched_msg(acfactory, lp, tmp_path):
|
||||
message, then processes a partially downloaded message.
|
||||
- As a result, Bob does not see a reaction
|
||||
"""
|
||||
download_limit = 32768
|
||||
download_limit = 300000
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1_addr = ac1.get_config("addr")
|
||||
chat = ac1.create_chat(ac2)
|
||||
@@ -1699,6 +1699,59 @@ def test_qr_join_chat(acfactory, lp, verified_one_on_one_chats):
|
||||
assert not ch.is_protected()
|
||||
|
||||
|
||||
def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory, lp):
|
||||
ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4)
|
||||
|
||||
lp.sec("ac3: verify with ac2")
|
||||
ac3.qr_setup_contact(ac2.get_setup_contact_qr())
|
||||
ac2._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
|
||||
# in order for ac2 to have pending bobstate with a verified group
|
||||
# we first create a fully joined verified group, and then start
|
||||
# joining a second time but interrupt it, to create pending bob state
|
||||
|
||||
lp.sec("ac1: create verified group that ac2 fully joins")
|
||||
ch1 = ac1.create_group_chat("ac1-shutoff group", verified=True)
|
||||
ac2.qr_join_chat(ch1.get_join_qr())
|
||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
|
||||
# ensure ac1 can write and ac2 receives messages in verified chat
|
||||
ch1.send_text("ac1 says hello")
|
||||
while 1:
|
||||
msg = ac2.wait_next_incoming_message()
|
||||
if msg.text == "ac1 says hello":
|
||||
assert msg.chat.is_protected()
|
||||
break
|
||||
|
||||
lp.sec("ac1: let ac2 join again but shutoff ac1 in the middle of securejoin")
|
||||
ac2.qr_join_chat(ch1.get_join_qr())
|
||||
ac1.shutdown()
|
||||
lp.sec("ac2 now has pending bobstate but ac1 is shutoff")
|
||||
|
||||
# we meanwhile expect ac3/ac2 verification started in the beginning to have completed
|
||||
assert ac3.get_contact(ac2).is_verified()
|
||||
assert ac2.get_contact(ac3).is_verified()
|
||||
|
||||
lp.sec("ac3: create a verified group VG with ac2")
|
||||
vg = ac3.create_group_chat("ac3-created", [ac2], verified=True)
|
||||
|
||||
# ensure ac2 receives message in VG
|
||||
vg.send_text("hello")
|
||||
while 1:
|
||||
msg = ac2.wait_next_incoming_message()
|
||||
if msg.text == "hello":
|
||||
assert msg.chat.is_protected()
|
||||
break
|
||||
|
||||
lp.sec("ac3: create a join-code for group VG and let ac4 join, check that ac2 got it")
|
||||
ac4.qr_join_chat(vg.get_join_qr())
|
||||
ac3._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
while 1:
|
||||
ev = ac2._evtracker.get()
|
||||
if "added by unrelated SecureJoin" in str(ev):
|
||||
return
|
||||
|
||||
|
||||
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.
|
||||
@@ -1904,7 +1957,7 @@ def test_system_group_msg_from_blocked_user(acfactory, lp):
|
||||
chat_on_ac2.send_text("This will arrive")
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "This will arrive"
|
||||
message_texts = [m.text for m in chat_on_ac1.get_messages()]
|
||||
message_texts = [m.text for m in chat_on_ac1.get_messages() if not m.is_system_message()]
|
||||
assert len(message_texts) == 2
|
||||
assert "First group message" in message_texts
|
||||
assert "This will arrive" in message_texts
|
||||
|
||||
@@ -26,7 +26,7 @@ def wait_msgs_changed(account, msgs_list):
|
||||
break
|
||||
else:
|
||||
account.log(f"waiting mismatch data1={data1} data2={data2}")
|
||||
return ev.data1, ev.data2
|
||||
return ev.data2
|
||||
|
||||
|
||||
class TestOnlineInCreation:
|
||||
@@ -68,14 +68,17 @@ class TestOnlineInCreation:
|
||||
assert prepared_original.is_out_preparing()
|
||||
wait_msgs_changed(ac1, [(chat.id, prepared_original.id)])
|
||||
|
||||
lp.sec("forward the message while still in creation")
|
||||
lp.sec("create a new group")
|
||||
chat2 = ac1.create_group_chat("newgroup")
|
||||
wait_msgs_changed(ac1, [(0, 0)])
|
||||
|
||||
lp.sec("add a contact to new group")
|
||||
chat2.add_contact(ac2)
|
||||
wait_msgs_changed(ac1, [(0, 0)]) # why not chat id?
|
||||
wait_msgs_changed(ac1, [(chat2.id, None)])
|
||||
|
||||
lp.sec("forward the message while still in creation")
|
||||
ac1.forward_messages([prepared_original], chat2)
|
||||
# XXX there might be two EVENT_MSGS_CHANGED and only one of them
|
||||
# is the one caused by forwarding
|
||||
_, forwarded_id = wait_msgs_changed(ac1, [(chat2.id, None)])
|
||||
forwarded_id = wait_msgs_changed(ac1, [(chat2.id, None)])
|
||||
forwarded_msg = ac1.get_message_by_id(forwarded_id)
|
||||
assert forwarded_msg.is_out_preparing()
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@ from datetime import datetime, timedelta, timezone
|
||||
import pytest
|
||||
|
||||
import deltachat as dc
|
||||
from deltachat.capi import ffi, lib
|
||||
from deltachat.cutil import iter_array
|
||||
from deltachat.tracker import ImexFailed
|
||||
from deltachat import Account, account_hookimpl, Message
|
||||
|
||||
@@ -802,6 +800,7 @@ class TestOfflineChat:
|
||||
def test_audit_log_view_without_daymarker(self, ac1, lp):
|
||||
lp.sec("ac1: test audit log (show only system messages)")
|
||||
chat = ac1.create_group_chat(name="audit log sample data")
|
||||
|
||||
# promote chat
|
||||
chat.send_text("hello")
|
||||
assert chat.is_promoted()
|
||||
@@ -811,12 +810,6 @@ class TestOfflineChat:
|
||||
chat.set_name("audit log test group")
|
||||
chat.send_text("a message in between")
|
||||
|
||||
lp.sec("check message count of all messages")
|
||||
assert len(chat.get_messages()) == 4
|
||||
|
||||
lp.sec("check message count of only system messages (without daymarkers)")
|
||||
dc_array = ffi.gc(
|
||||
lib.dc_get_chat_msgs(ac1._dc_context, chat.id, dc.const.DC_GCM_INFO_ONLY, 0),
|
||||
lib.dc_array_unref,
|
||||
)
|
||||
assert len(list(iter_array(dc_array, lambda x: x))) == 2
|
||||
sysmessages = [x for x in chat.get_messages() if x.is_system_message()]
|
||||
assert len(sysmessages) == 3
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2023-10-29
|
||||
2023-11-06
|
||||
@@ -6,7 +6,7 @@ set -euo pipefail
|
||||
export TZ=UTC
|
||||
|
||||
# Provider database revision.
|
||||
REV=3c8f7e846c915a183dc44536fb5480d1f25d7c42
|
||||
REV=18f714cf73d0bdfb8b013fa344494ab80c92b477
|
||||
|
||||
CORE_ROOT="$PWD"
|
||||
TMP="$(mktemp -d)"
|
||||
|
||||
+2
-2
@@ -258,8 +258,8 @@ impl Accounts {
|
||||
}
|
||||
|
||||
/// Starts background tasks such as IMAP and SMTP loops for all accounts.
|
||||
pub async fn start_io(&self) {
|
||||
for account in self.accounts.values() {
|
||||
pub async fn start_io(&mut self) {
|
||||
for account in self.accounts.values_mut() {
|
||||
account.start_io().await;
|
||||
}
|
||||
}
|
||||
|
||||
+191
-55
@@ -10,6 +10,7 @@ use std::time::{Duration, SystemTime};
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum_macros::EnumIter;
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::blob::BlobObject;
|
||||
@@ -20,7 +21,7 @@ use crate::constants::{
|
||||
Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CHAT_ID_LAST_SPECIAL,
|
||||
DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS,
|
||||
};
|
||||
use crate::contact::{Contact, ContactId, Origin, VerifiedStatus};
|
||||
use crate::contact::{self, Contact, ContactId, Origin, VerifiedStatus};
|
||||
use crate::context::Context;
|
||||
use crate::debug_logging::maybe_set_logging_xdc;
|
||||
use crate::download::DownloadState;
|
||||
@@ -38,6 +39,7 @@ use crate::scheduler::InterruptInfo;
|
||||
use crate::smtp::send_msg_to_smtp;
|
||||
use crate::sql;
|
||||
use crate::stock_str;
|
||||
use crate::sync::{self, ChatAction, Sync::*, SyncData};
|
||||
use crate::tools::{
|
||||
buf_compress, create_id, create_outgoing_rfc724_mid, create_smeared_timestamp,
|
||||
create_smeared_timestamps, get_abs_path, gm2local_offset, improve_single_line_input,
|
||||
@@ -206,9 +208,10 @@ impl ChatId {
|
||||
self == DC_CHAT_ID_ALLDONE_HINT
|
||||
}
|
||||
|
||||
/// Returns the [`ChatId`] for the 1:1 chat with `contact_id` if it exists.
|
||||
/// Returns the [`ChatId`] for the 1:1 chat with `contact_id`
|
||||
/// if it exists and is not blocked.
|
||||
///
|
||||
/// If it does not exist, `None` is returned.
|
||||
/// If the chat does not exist or is blocked, `None` is returned.
|
||||
pub async fn lookup_by_contact(
|
||||
context: &Context,
|
||||
contact_id: ContactId,
|
||||
@@ -225,7 +228,7 @@ impl ChatId {
|
||||
/// This is an internal API, if **a user action** needs to get a chat
|
||||
/// [`ChatId::create_for_contact`] should be used as this also scales up the
|
||||
/// [`Contact`]'s origin.
|
||||
pub async fn get_for_contact(context: &Context, contact_id: ContactId) -> Result<Self> {
|
||||
pub(crate) async fn get_for_contact(context: &Context, contact_id: ContactId) -> Result<Self> {
|
||||
ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Not)
|
||||
.await
|
||||
.map(|chat| chat.id)
|
||||
@@ -250,7 +253,7 @@ impl ChatId {
|
||||
let chat_id = match ChatIdBlocked::lookup_by_contact(context, contact_id).await? {
|
||||
Some(chat) => {
|
||||
if create_blocked == Blocked::Not && chat.blocked != Blocked::Not {
|
||||
chat.id.unblock(context).await?;
|
||||
chat.id.set_blocked(context, Blocked::Not).await?;
|
||||
}
|
||||
chat.id
|
||||
}
|
||||
@@ -356,6 +359,10 @@ impl ChatId {
|
||||
|
||||
/// Blocks the chat as a result of explicit user action.
|
||||
pub async fn block(self, context: &Context) -> Result<()> {
|
||||
self.block_ex(context, Sync).await
|
||||
}
|
||||
|
||||
pub(crate) async fn block_ex(self, context: &Context, sync: sync::Sync) -> Result<()> {
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
|
||||
match chat.typ {
|
||||
@@ -369,7 +376,7 @@ impl ChatId {
|
||||
context,
|
||||
"Blocking the contact {contact_id} to block 1:1 chat."
|
||||
);
|
||||
Contact::block(context, contact_id).await?;
|
||||
contact::set_blocked(context, Nosync, contact_id, true).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -384,12 +391,28 @@ impl ChatId {
|
||||
}
|
||||
}
|
||||
|
||||
if sync.into() {
|
||||
// NB: For a 1:1 chat this currently triggers `Contact::block()` on other devices.
|
||||
chat.add_sync_item(context, ChatAction::Block).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Unblocks the chat.
|
||||
pub async fn unblock(self, context: &Context) -> Result<()> {
|
||||
self.unblock_ex(context, Sync).await
|
||||
}
|
||||
|
||||
pub(crate) async fn unblock_ex(self, context: &Context, sync: sync::Sync) -> Result<()> {
|
||||
self.set_blocked(context, Blocked::Not).await?;
|
||||
|
||||
if sync.into() {
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
// TODO: For a 1:1 chat this currently triggers `Contact::unblock()` on other devices.
|
||||
// Maybe we should unblock the contact locally too, this would also resolve discrepancy
|
||||
// with `block()` which also blocks the contact.
|
||||
chat.add_sync_item(context, ChatAction::Unblock).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -397,6 +420,10 @@ impl ChatId {
|
||||
///
|
||||
/// Unblocks the chat and scales up origin of contacts.
|
||||
pub async fn accept(self, context: &Context) -> Result<()> {
|
||||
self.accept_ex(context, Sync).await
|
||||
}
|
||||
|
||||
pub(crate) async fn accept_ex(self, context: &Context, sync: sync::Sync) -> Result<()> {
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
|
||||
match chat.typ {
|
||||
@@ -431,6 +458,9 @@ impl ChatId {
|
||||
context.emit_event(EventType::ChatModified(self));
|
||||
}
|
||||
|
||||
if sync.into() {
|
||||
chat.add_sync_item(context, ChatAction::Accept).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -533,6 +563,15 @@ impl ChatId {
|
||||
|
||||
/// Archives or unarchives a chat.
|
||||
pub async fn set_visibility(self, context: &Context, visibility: ChatVisibility) -> Result<()> {
|
||||
self.set_visibility_ex(context, Sync, visibility).await
|
||||
}
|
||||
|
||||
pub(crate) async fn set_visibility_ex(
|
||||
self,
|
||||
context: &Context,
|
||||
sync: sync::Sync,
|
||||
visibility: ChatVisibility,
|
||||
) -> Result<()> {
|
||||
ensure!(
|
||||
!self.is_special(),
|
||||
"bad chat_id, can not be special chat: {}",
|
||||
@@ -558,6 +597,11 @@ impl ChatId {
|
||||
|
||||
context.emit_msgs_changed_without_ids();
|
||||
|
||||
if sync.into() {
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
chat.add_sync_item(context, ChatAction::SetVisibility(visibility))
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -630,7 +674,6 @@ impl ChatId {
|
||||
"bad chat_id, can not be a special chat: {}",
|
||||
self
|
||||
);
|
||||
/* Up to 2017-11-02 deleting a group also implied leaving it, see above why we have changed this. */
|
||||
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
context
|
||||
@@ -1209,6 +1252,16 @@ impl ChatId {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns true if the chat is protected.
|
||||
pub async fn is_protected(self, context: &Context) -> Result<ProtectionStatus> {
|
||||
let protection_status = context
|
||||
.sql
|
||||
.query_get_value("SELECT protected FROM chats WHERE id=?", (self,))
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
Ok(protection_status)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ChatId {
|
||||
@@ -1270,7 +1323,8 @@ pub struct Chat {
|
||||
/// Whether the chat is archived or pinned.
|
||||
pub visibility: ChatVisibility,
|
||||
|
||||
/// Group ID.
|
||||
/// Group ID. For [`Chattype::Mailinglist`] -- mailing list address. Empty for 1:1 chats and
|
||||
/// ad-hoc groups.
|
||||
pub grpid: String,
|
||||
|
||||
/// Whether the chat is blocked, unblocked or a contact request.
|
||||
@@ -1451,12 +1505,6 @@ impl Chat {
|
||||
if let Ok(image_rel) = get_archive_icon(context).await {
|
||||
return Ok(Some(get_abs_path(context, Path::new(&image_rel))));
|
||||
}
|
||||
}
|
||||
|
||||
if self.typ == Chattype::Group {
|
||||
if let Ok(image_rel) = get_group_icon(context).await {
|
||||
return Ok(Some(get_abs_path(context, Path::new(&image_rel))));
|
||||
}
|
||||
} else if self.typ == Chattype::Single {
|
||||
let contacts = get_chat_contacts(context, self.id).await?;
|
||||
if let Some(contact_id) = contacts.first() {
|
||||
@@ -1547,6 +1595,15 @@ impl Chat {
|
||||
}
|
||||
|
||||
/// Returns true if chat protection is enabled.
|
||||
///
|
||||
/// UI should display a green checkmark
|
||||
/// in the chat title,
|
||||
/// in the chat profile title and
|
||||
/// in the chatlist item
|
||||
/// if chat protection is enabled.
|
||||
/// UI should also display a green checkmark
|
||||
/// in the contact profile
|
||||
/// if 1:1 chat with this contact exists and is protected.
|
||||
pub fn is_protected(&self) -> bool {
|
||||
self.protected == ProtectionStatus::Protected
|
||||
}
|
||||
@@ -1833,29 +1890,62 @@ impl Chat {
|
||||
context.scheduler.interrupt_ephemeral_task().await;
|
||||
Ok(msg.id)
|
||||
}
|
||||
|
||||
/// Returns chat id for the purpose of synchronisation across devices.
|
||||
async fn get_sync_id(&self, context: &Context) -> Result<Option<sync::ChatId>> {
|
||||
match self.typ {
|
||||
Chattype::Single => {
|
||||
let mut r = None;
|
||||
for contact_id in get_chat_contacts(context, self.id).await? {
|
||||
if contact_id == ContactId::SELF && !self.is_self_talk() {
|
||||
continue;
|
||||
}
|
||||
if r.is_some() {
|
||||
return Ok(None);
|
||||
}
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
r = Some(sync::ChatId::ContactAddr(contact.get_addr().to_string()));
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
Chattype::Broadcast | Chattype::Group | Chattype::Mailinglist => {
|
||||
if self.grpid.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(sync::ChatId::Grpid(self.grpid.clone())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a chat action to the list of items to synchronise to other devices.
|
||||
pub(crate) async fn add_sync_item(&self, context: &Context, action: ChatAction) -> Result<()> {
|
||||
if let Some(id) = self.get_sync_id(context).await? {
|
||||
context
|
||||
.add_sync_item(SyncData::AlterChat { id, action })
|
||||
.await?;
|
||||
context.send_sync_msg().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the chat is pinned or archived.
|
||||
#[derive(Debug, Copy, Eq, PartialEq, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Copy, Eq, PartialEq, Clone, Serialize, Deserialize, EnumIter)]
|
||||
#[repr(i8)]
|
||||
pub enum ChatVisibility {
|
||||
/// Chat is neither archived nor pinned.
|
||||
Normal,
|
||||
Normal = 0,
|
||||
|
||||
/// Chat is archived.
|
||||
Archived,
|
||||
Archived = 1,
|
||||
|
||||
/// Chat is pinned to the top of the chatlist.
|
||||
Pinned,
|
||||
Pinned = 2,
|
||||
}
|
||||
|
||||
impl rusqlite::types::ToSql for ChatVisibility {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
let visibility = match &self {
|
||||
ChatVisibility::Normal => 0,
|
||||
ChatVisibility::Archived => 1,
|
||||
ChatVisibility::Pinned => 2,
|
||||
};
|
||||
let val = rusqlite::types::Value::Integer(visibility);
|
||||
let val = rusqlite::types::Value::Integer(*self as i64);
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
}
|
||||
@@ -1989,21 +2079,6 @@ pub(crate) async fn get_broadcast_icon(context: &Context) -> Result<String> {
|
||||
Ok(icon)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_group_icon(context: &Context) -> Result<String> {
|
||||
if let Some(icon) = context.sql.get_raw_config("icon-group").await? {
|
||||
return Ok(icon);
|
||||
}
|
||||
|
||||
let icon = include_bytes!("../assets/icon-group.png");
|
||||
let blob = BlobObject::create(context, "icon-group.png", icon).await?;
|
||||
let icon = blob.as_name().to_string();
|
||||
context
|
||||
.sql
|
||||
.set_raw_config("icon-group", Some(&icon))
|
||||
.await?;
|
||||
Ok(icon)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_archive_icon(context: &Context) -> Result<String> {
|
||||
if let Some(icon) = context.sql.get_raw_config("icon-archive").await? {
|
||||
return Ok(icon);
|
||||
@@ -3081,6 +3156,13 @@ pub async fn create_group_chat(
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !context.get_config_bool(Config::Bot).await?
|
||||
&& !context.get_config_bool(Config::SkipStartMessages).await?
|
||||
{
|
||||
let text = stock_str::new_group_send_first_message(context).await;
|
||||
add_info_msg(context, chat_id, &text, create_smeared_timestamp(context)).await?;
|
||||
}
|
||||
|
||||
Ok(chat_id)
|
||||
}
|
||||
|
||||
@@ -3301,7 +3383,7 @@ pub(crate) async fn shall_attach_selfavatar(context: &Context, chat_id: ChatId)
|
||||
}
|
||||
|
||||
/// Chat mute duration.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum MuteDuration {
|
||||
/// Chat is not muted.
|
||||
NotMuted,
|
||||
@@ -3350,6 +3432,15 @@ impl rusqlite::types::FromSql for MuteDuration {
|
||||
|
||||
/// Mutes the chat for a given duration or unmutes it.
|
||||
pub async fn set_muted(context: &Context, chat_id: ChatId, duration: MuteDuration) -> Result<()> {
|
||||
set_muted_ex(context, Sync, chat_id, duration).await
|
||||
}
|
||||
|
||||
pub(crate) async fn set_muted_ex(
|
||||
context: &Context,
|
||||
sync: sync::Sync,
|
||||
chat_id: ChatId,
|
||||
duration: MuteDuration,
|
||||
) -> Result<()> {
|
||||
ensure!(!chat_id.is_special(), "Invalid chat ID");
|
||||
context
|
||||
.sql
|
||||
@@ -3360,6 +3451,11 @@ pub async fn set_muted(context: &Context, chat_id: ChatId, duration: MuteDuratio
|
||||
.await
|
||||
.context(format!("Failed to set mute duration for {chat_id}"))?;
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
if sync.into() {
|
||||
let chat = Chat::load_from_db(context, chat_id).await?;
|
||||
chat.add_sync_item(context, ChatAction::SetMuted(duration))
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3984,6 +4080,56 @@ pub(crate) async fn update_msg_text_and_timestamp(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Executes [`SyncData::AlterChat`] item sent by other device.
|
||||
pub(crate) async fn sync_alter_chat(
|
||||
&self,
|
||||
id: &sync::ChatId,
|
||||
action: &ChatAction,
|
||||
) -> Result<()> {
|
||||
let chat_id = match id {
|
||||
sync::ChatId::ContactAddr(addr) => {
|
||||
let Some(contact_id) =
|
||||
Contact::lookup_id_by_addr_ex(self, addr, Origin::Unknown, None).await?
|
||||
else {
|
||||
warn!(self, "sync_alter_chat: No contact for addr '{addr}'.");
|
||||
return Ok(());
|
||||
};
|
||||
match action {
|
||||
ChatAction::Block => {
|
||||
return contact::set_blocked(self, Nosync, contact_id, true).await
|
||||
}
|
||||
ChatAction::Unblock => {
|
||||
return contact::set_blocked(self, Nosync, contact_id, false).await
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
let Some(chat_id) = ChatId::lookup_by_contact(self, contact_id).await? else {
|
||||
warn!(self, "sync_alter_chat: No chat for addr '{addr}'.");
|
||||
return Ok(());
|
||||
};
|
||||
chat_id
|
||||
}
|
||||
sync::ChatId::Grpid(grpid) => {
|
||||
let Some((chat_id, ..)) = get_chat_id_by_grpid(self, grpid).await? else {
|
||||
warn!(self, "sync_alter_chat: No chat for grpid '{grpid}'.");
|
||||
return Ok(());
|
||||
};
|
||||
chat_id
|
||||
}
|
||||
};
|
||||
match action {
|
||||
ChatAction::Block => chat_id.block_ex(self, Nosync).await,
|
||||
ChatAction::Unblock => chat_id.unblock_ex(self, Nosync).await,
|
||||
ChatAction::Accept => chat_id.accept_ex(self, Nosync).await,
|
||||
ChatAction::SetVisibility(v) => chat_id.set_visibility_ex(self, Nosync, *v).await,
|
||||
ChatAction::SetMuted(duration) => set_muted_ex(self, Nosync, chat_id, *duration).await,
|
||||
}
|
||||
.ok();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -4327,13 +4473,11 @@ mod tests {
|
||||
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
|
||||
|
||||
// Bob receives a msg about Alice adding Claire to the group.
|
||||
let bob_received_add_msg = bob.recv_msg(&alice_sent_add_msg).await;
|
||||
bob.recv_msg(&alice_sent_add_msg).await;
|
||||
|
||||
// Test that add message is rewritten.
|
||||
assert_eq!(
|
||||
bob_received_add_msg.get_text(),
|
||||
"Member claire@example.net added by alice@example.org."
|
||||
);
|
||||
bob.golden_test_chat(bob_chat_id, "chat_test_simultaneous_member_remove")
|
||||
.await;
|
||||
|
||||
// Bob receives a msg about Alice removing him from the group.
|
||||
let bob_received_remove_msg = bob.recv_msg(&alice_sent_remove_msg).await;
|
||||
@@ -4368,16 +4512,8 @@ mod tests {
|
||||
assert_eq!(a1_chat.grpid, a2_chat.grpid);
|
||||
assert_eq!(a1_chat.name, "foo");
|
||||
assert_eq!(a2_chat.name, "foo");
|
||||
assert!(a1_chat
|
||||
.get_profile_image(&a1)
|
||||
.await?
|
||||
.unwrap()
|
||||
.ends_with("icon-group.png"));
|
||||
assert!(a2_chat
|
||||
.get_profile_image(&a2)
|
||||
.await?
|
||||
.unwrap()
|
||||
.ends_with("icon-group.png"));
|
||||
assert_eq!(a1_chat.get_profile_image(&a1).await?, None);
|
||||
assert_eq!(a2_chat.get_profile_image(&a2).await?, None);
|
||||
assert_eq!(get_chat_contacts(&a1, a1_chat_id).await?.len(), 1);
|
||||
assert_eq!(get_chat_contacts(&a2, a2_chat_id).await?.len(), 1);
|
||||
|
||||
|
||||
+5
-1
@@ -266,6 +266,10 @@ pub enum Config {
|
||||
/// True if it is a bot account.
|
||||
Bot,
|
||||
|
||||
/// True when to skip initial start messages in groups.
|
||||
#[strum(props(default = "0"))]
|
||||
SkipStartMessages,
|
||||
|
||||
/// Whether we send a warning if the password is wrong (set to false when we send a warning
|
||||
/// because we do not want to send a second warning)
|
||||
#[strum(props(default = "0"))]
|
||||
@@ -298,7 +302,7 @@ pub enum Config {
|
||||
DownloadLimit,
|
||||
|
||||
/// Enable sending and executing (applying) sync messages. Sending requires `BccSelf` to be set.
|
||||
#[strum(props(default = "0"))]
|
||||
#[strum(props(default = "1"))]
|
||||
SyncMsgs,
|
||||
|
||||
/// Space-separated list of all the authserv-ids which we believe
|
||||
|
||||
+90
-23
@@ -19,7 +19,7 @@ use tokio::task;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::chat::ChatId;
|
||||
use crate::chat::{ChatId, ProtectionStatus};
|
||||
use crate::color::str_to_color;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY};
|
||||
@@ -32,6 +32,7 @@ use crate::mimeparser::AvatarAction;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
|
||||
use crate::sql::{self, params_iter};
|
||||
use crate::sync::{self, Sync::*, SyncData};
|
||||
use crate::tools::{
|
||||
duration_to_str, get_abs_path, improve_single_line_input, strip_rtlo_characters, time,
|
||||
EmailAddress,
|
||||
@@ -459,12 +460,12 @@ impl Contact {
|
||||
|
||||
/// Block the given contact.
|
||||
pub async fn block(context: &Context, id: ContactId) -> Result<()> {
|
||||
set_block_contact(context, id, true).await
|
||||
set_blocked(context, Sync, id, true).await
|
||||
}
|
||||
|
||||
/// Unblock the given contact.
|
||||
pub async fn unblock(context: &Context, id: ContactId) -> Result<()> {
|
||||
set_block_contact(context, id, false).await
|
||||
set_blocked(context, Sync, id, false).await
|
||||
}
|
||||
|
||||
/// Add a single contact as a result of an _explicit_ user action.
|
||||
@@ -494,7 +495,7 @@ impl Contact {
|
||||
}
|
||||
}
|
||||
if blocked {
|
||||
Contact::unblock(context, contact_id).await?;
|
||||
set_blocked(context, Nosync, contact_id, false).await?;
|
||||
}
|
||||
|
||||
Ok(contact_id)
|
||||
@@ -523,10 +524,24 @@ impl Contact {
|
||||
///
|
||||
/// To validate an e-mail address independently of the contact database
|
||||
/// use `may_be_valid_addr()`.
|
||||
///
|
||||
/// Returns the contact ID of the contact belonging to the e-mail address or 0 if there is no
|
||||
/// contact that is or was introduced by an accepted contact.
|
||||
pub async fn lookup_id_by_addr(
|
||||
context: &Context,
|
||||
addr: &str,
|
||||
min_origin: Origin,
|
||||
) -> Result<Option<ContactId>> {
|
||||
Self::lookup_id_by_addr_ex(context, addr, min_origin, Some(Blocked::Not)).await
|
||||
}
|
||||
|
||||
/// The same as `lookup_id_by_addr()`, but internal function. Currently also allows looking up
|
||||
/// not unblocked contacts.
|
||||
pub(crate) async fn lookup_id_by_addr_ex(
|
||||
context: &Context,
|
||||
addr: &str,
|
||||
min_origin: Origin,
|
||||
blocked: Option<Blocked>,
|
||||
) -> Result<Option<ContactId>> {
|
||||
if addr.is_empty() {
|
||||
bail!("lookup_id_by_addr: empty address");
|
||||
@@ -543,8 +558,14 @@ impl Contact {
|
||||
.query_get_value(
|
||||
"SELECT id FROM contacts \
|
||||
WHERE addr=?1 COLLATE NOCASE \
|
||||
AND id>?2 AND origin>=?3 AND blocked=0;",
|
||||
(&addr_normalized, ContactId::LAST_SPECIAL, min_origin as u32),
|
||||
AND id>?2 AND origin>=?3 AND (? OR blocked=?)",
|
||||
(
|
||||
&addr_normalized,
|
||||
ContactId::LAST_SPECIAL,
|
||||
min_origin as u32,
|
||||
blocked.is_none(),
|
||||
blocked.unwrap_or_default(),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
Ok(id)
|
||||
@@ -1230,10 +1251,21 @@ impl Contact {
|
||||
self.status.as_str()
|
||||
}
|
||||
|
||||
/// Check if a contact was verified. E.g. by a secure-join QR code scan
|
||||
/// and if the key has not changed since this verification.
|
||||
/// Returns true if the contact
|
||||
/// can be added to verified chats,
|
||||
/// i.e. has a verified key
|
||||
/// and Autocrypt key matches the verified key.
|
||||
///
|
||||
/// The UI may draw a checkbox or something like that beside verified contacts.
|
||||
/// If contact is verified
|
||||
/// UI should display green checkmark after the contact name
|
||||
/// in contact list items and
|
||||
/// in chat member list items.
|
||||
///
|
||||
/// Do not use this function when displaying the contact profile view.
|
||||
/// Display green checkmark in the title of the contact profile
|
||||
/// if [Self::is_profile_verified] returns true.
|
||||
/// Use [Self::get_verifier_id] to display the verifier contact
|
||||
/// in the info section of the contact profile.
|
||||
pub async fn is_verified(&self, context: &Context) -> Result<VerifiedStatus> {
|
||||
// We're always sort of secured-verified as we could verify the key on this device any time with the key
|
||||
// on this device
|
||||
@@ -1250,16 +1282,23 @@ impl Contact {
|
||||
Ok(VerifiedStatus::Unverified)
|
||||
}
|
||||
|
||||
/// Returns the address that verified the contact.
|
||||
pub async fn get_verifier_addr(&self, context: &Context) -> Result<Option<String>> {
|
||||
Ok(Peerstate::from_addr(context, self.get_addr())
|
||||
.await?
|
||||
.and_then(|peerstate| peerstate.get_verifier().map(|addr| addr.to_owned())))
|
||||
}
|
||||
|
||||
/// Returns the ContactId that verified the contact.
|
||||
/// Returns the `ContactId` that verified the contact.
|
||||
///
|
||||
/// If the function returns non-zero result,
|
||||
/// display green checkmark in the profile and "Introduced by ..." line
|
||||
/// with the name and address of the contact
|
||||
/// formatted by [Self::get_name_n_addr].
|
||||
///
|
||||
/// If this function returns a verifier,
|
||||
/// this does not necessarily mean
|
||||
/// you can add the contact to verified chats.
|
||||
/// Use [Self::is_verified] to check
|
||||
/// if a contact can be added to a verified chat instead.
|
||||
pub async fn get_verifier_id(&self, context: &Context) -> Result<Option<ContactId>> {
|
||||
let Some(verifier_addr) = self.get_verifier_addr(context).await? else {
|
||||
let Some(verifier_addr) = Peerstate::from_addr(context, self.get_addr())
|
||||
.await?
|
||||
.and_then(|peerstate| peerstate.get_verifier().map(|addr| addr.to_owned()))
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
@@ -1278,6 +1317,22 @@ impl Contact {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns if the contact profile title should display a green checkmark.
|
||||
///
|
||||
/// This generally should be consistent with the 1:1 chat with the contact
|
||||
/// so 1:1 chat with the contact and the contact profile
|
||||
/// either both display the green checkmark or both don't display a green checkmark.
|
||||
pub async fn is_profile_verified(&self, context: &Context) -> Result<bool> {
|
||||
let contact_id = self.id;
|
||||
|
||||
if let Some(chat_id) = ChatId::lookup_by_contact(context, contact_id).await? {
|
||||
Ok(chat_id.is_protected(context).await? == ProtectionStatus::Protected)
|
||||
} else {
|
||||
// 1:1 chat does not exist.
|
||||
Ok(self.is_verified(context).await? == VerifiedStatus::BidirectVerified)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of real (i.e. non-special) contacts in the database.
|
||||
pub async fn get_real_cnt(context: &Context) -> Result<usize> {
|
||||
if !context.sql.is_open().await {
|
||||
@@ -1363,8 +1418,9 @@ fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
||||
}
|
||||
}
|
||||
|
||||
async fn set_block_contact(
|
||||
pub(crate) async fn set_blocked(
|
||||
context: &Context,
|
||||
sync: sync::Sync,
|
||||
contact_id: ContactId,
|
||||
new_blocking: bool,
|
||||
) -> Result<()> {
|
||||
@@ -1373,7 +1429,6 @@ async fn set_block_contact(
|
||||
"Can't block special contact {}",
|
||||
contact_id
|
||||
);
|
||||
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
|
||||
if contact.blocked != new_blocking {
|
||||
@@ -1415,9 +1470,23 @@ WHERE type=? AND id IN (
|
||||
if let Some((chat_id, _, _)) =
|
||||
chat::get_chat_id_by_grpid(context, &contact.addr).await?
|
||||
{
|
||||
chat_id.unblock(context).await?;
|
||||
chat_id.unblock_ex(context, Nosync).await?;
|
||||
}
|
||||
}
|
||||
|
||||
if sync.into() {
|
||||
let action = match new_blocking {
|
||||
true => sync::ChatAction::Block,
|
||||
false => sync::ChatAction::Unblock,
|
||||
};
|
||||
context
|
||||
.add_sync_item(SyncData::AlterChat {
|
||||
id: sync::ChatId::ContactAddr(contact.addr.clone()),
|
||||
action,
|
||||
})
|
||||
.await?;
|
||||
context.send_sync_msg().await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -2710,7 +2779,6 @@ Hi."#;
|
||||
|
||||
let contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
|
||||
let contact = Contact::get_by_id(&alice, contact_id).await?;
|
||||
assert!(contact.get_verifier_addr(&alice).await?.is_none());
|
||||
assert!(contact.get_verifier_id(&alice).await?.is_none());
|
||||
|
||||
// Receive a message from Bob to create a peerstate.
|
||||
@@ -2719,7 +2787,6 @@ Hi."#;
|
||||
alice.recv_msg(&sent_msg).await;
|
||||
|
||||
let contact = Contact::get_by_id(&alice, contact_id).await?;
|
||||
assert!(contact.get_verifier_addr(&alice).await?.is_none());
|
||||
assert!(contact.get_verifier_id(&alice).await?.is_none());
|
||||
|
||||
Ok(())
|
||||
|
||||
+16
-2
@@ -398,11 +398,24 @@ impl Context {
|
||||
}
|
||||
|
||||
/// Starts the IO scheduler.
|
||||
pub async fn start_io(&self) {
|
||||
if let Ok(false) = self.is_configured().await {
|
||||
pub async fn start_io(&mut self) {
|
||||
if !self.is_configured().await.unwrap_or_default() {
|
||||
warn!(self, "can not start io on a context that is not configured");
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
if self
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.filter(|s| s.ends_with(".testrun.org"))
|
||||
.is_some()
|
||||
{
|
||||
let mut lock = self.ratelimit.write().await;
|
||||
*lock = Ratelimit::new(Duration::new(40, 0), 6.0);
|
||||
}
|
||||
}
|
||||
self.scheduler.start(self.clone()).await;
|
||||
}
|
||||
|
||||
@@ -1302,6 +1315,7 @@ mod tests {
|
||||
"send_port",
|
||||
"send_security",
|
||||
"server_flags",
|
||||
"skip_start_messages",
|
||||
"smtp_certificate_checks",
|
||||
"socks5_host",
|
||||
"socks5_port",
|
||||
|
||||
+5
-6
@@ -18,12 +18,11 @@ use crate::{stock_str, EventType};
|
||||
|
||||
/// Download limits should not be used below `MIN_DOWNLOAD_LIMIT`.
|
||||
///
|
||||
/// Some messages as non-delivery-reports (NDN) or read-receipts (MDN)
|
||||
/// need to be downloaded completely to handle them correctly,
|
||||
/// eg. to assign them to the correct chat.
|
||||
/// As these messages are typically small,
|
||||
/// they're caught by `MIN_DOWNLOAD_LIMIT`.
|
||||
pub(crate) const MIN_DOWNLOAD_LIMIT: u32 = 32768;
|
||||
/// For better UX, some messages as add-member, non-delivery-reports (NDN) or read-receipts (MDN)
|
||||
/// should always be downloaded completely to handle them correctly,
|
||||
/// also in larger groups and if group and contact avatar are attached.
|
||||
/// Most of these cases are caught by `MIN_DOWNLOAD_LIMIT`.
|
||||
pub(crate) const MIN_DOWNLOAD_LIMIT: u32 = 163840;
|
||||
|
||||
/// If a message is downloaded only partially
|
||||
/// and `delete_server_after` is set to small timeouts (eg. "at once"),
|
||||
|
||||
+1
-6
@@ -67,13 +67,8 @@ impl EncryptHelper {
|
||||
"peerstate for {:?} is {}", addr, peerstate.prefer_encrypt
|
||||
);
|
||||
match peerstate.prefer_encrypt {
|
||||
EncryptPreference::NoPreference => {}
|
||||
EncryptPreference::NoPreference | EncryptPreference::Reset => {}
|
||||
EncryptPreference::Mutual => prefer_encrypt_count += 1,
|
||||
EncryptPreference::Reset => {
|
||||
if !e2ee_guaranteed {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
None => {
|
||||
|
||||
@@ -250,6 +250,7 @@ pub enum EventType {
|
||||
/// Progress as:
|
||||
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
||||
/// (Bob has verified alice and waits until Alice does the same for him)
|
||||
/// 1000=vg-member-added/vc-contact-confirm received
|
||||
progress: usize,
|
||||
},
|
||||
|
||||
|
||||
+65
-54
@@ -568,9 +568,14 @@ impl Imap {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Select a folder and take care of uidvalidity changes.
|
||||
/// Also, when selecting a folder for the first time, sets the uid_next to the current
|
||||
/// Selects a folder and takes care of UIDVALIDITY changes.
|
||||
///
|
||||
/// When selecting a folder for the first time, sets the uid_next to the current
|
||||
/// mailbox.uid_next so that no old emails are fetched.
|
||||
///
|
||||
/// Makes sure that UIDNEXT is known for `selected_mailbox`
|
||||
/// and errors out if UIDNEXT cannot be determined.
|
||||
///
|
||||
/// Returns Result<new_emails> (i.e. whether new emails arrived),
|
||||
/// if in doubt, returns new_emails=true so emails are fetched.
|
||||
pub(crate) async fn select_with_uidvalidity(
|
||||
@@ -591,6 +596,37 @@ impl Imap {
|
||||
let new_uid_validity = mailbox
|
||||
.uid_validity
|
||||
.with_context(|| format!("No UIDVALIDITY for folder {folder}"))?;
|
||||
let new_uid_next = if let Some(uid_next) = mailbox.uid_next {
|
||||
uid_next
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"SELECT response for IMAP folder {folder:?} has no UIDNEXT, fall back to STATUS command."
|
||||
);
|
||||
|
||||
// RFC 3501 says STATUS command SHOULD NOT be used
|
||||
// on the currently selected mailbox because the same
|
||||
// information can be obtained by other means,
|
||||
// such as reading SELECT response.
|
||||
//
|
||||
// However, it also says that UIDNEXT is REQUIRED
|
||||
// in the SELECT response and if we are here,
|
||||
// it is actually not returned.
|
||||
//
|
||||
// In particular, Winmail Pro Mail Server 5.1.0616
|
||||
// never returns UIDNEXT in SELECT response,
|
||||
// but responds to "STATUS INBOX (UIDNEXT)" command.
|
||||
let status = session
|
||||
.inner
|
||||
.status(folder, "(UIDNEXT)")
|
||||
.await
|
||||
.context("STATUS (UIDNEXT) error for {folder:?}")?;
|
||||
|
||||
status
|
||||
.uid_next
|
||||
.context("STATUS {folder} (UIDNEXT) did not return UIDNEXT")?
|
||||
};
|
||||
mailbox.uid_next = Some(new_uid_next);
|
||||
|
||||
let old_uid_validity = get_uidvalidity(context, folder)
|
||||
.await
|
||||
@@ -606,18 +642,16 @@ impl Imap {
|
||||
// the caller tries to fetch new messages (we could of course run a SELECT command now, but trying to fetch
|
||||
// new messages is only one command, just as a SELECT command)
|
||||
true
|
||||
} else if let Some(uid_next) = mailbox.uid_next {
|
||||
if uid_next < old_uid_next {
|
||||
} else {
|
||||
if new_uid_next < old_uid_next {
|
||||
warn!(
|
||||
context,
|
||||
"The server illegally decreased the uid_next of folder {folder:?} from {old_uid_next} to {uid_next} without changing validity ({new_uid_validity}), resyncing UIDs...",
|
||||
"The server illegally decreased the uid_next of folder {folder:?} from {old_uid_next} to {new_uid_next} without changing validity ({new_uid_validity}), resyncing UIDs...",
|
||||
);
|
||||
set_uid_next(context, folder, uid_next).await?;
|
||||
set_uid_next(context, folder, new_uid_next).await?;
|
||||
context.schedule_resync().await?;
|
||||
}
|
||||
uid_next != old_uid_next // If uid_next changed, there are new emails
|
||||
} else {
|
||||
true // We have no uid_next and if in doubt, return true
|
||||
new_uid_next != old_uid_next // If UIDNEXT changed, there are new emails
|
||||
};
|
||||
return Ok(new_emails);
|
||||
}
|
||||
@@ -627,43 +661,6 @@ impl Imap {
|
||||
|
||||
// ============== uid_validity has changed or is being set the first time. ==============
|
||||
|
||||
let new_uid_next = match mailbox.uid_next {
|
||||
Some(uid_next) => uid_next,
|
||||
None => {
|
||||
warn!(
|
||||
context,
|
||||
"SELECT response for IMAP folder {folder:?} has no UIDNEXT, fall back to STATUS command."
|
||||
);
|
||||
|
||||
// RFC 3501 says STATUS command SHOULD NOT be used
|
||||
// on the currently selected mailbox because the same
|
||||
// information can be obtained by other means,
|
||||
// such as reading SELECT response.
|
||||
//
|
||||
// However, it also says that UIDNEXT is REQUIRED
|
||||
// in the SELECT response and if we are here,
|
||||
// it is actually not returned.
|
||||
//
|
||||
// In particular, Winmail Pro Mail Server 5.1.0616
|
||||
// never returns UIDNEXT in SELECT response,
|
||||
// but responds to "SELECT INBOX (UIDNEXT)" command.
|
||||
let status = session
|
||||
.inner
|
||||
.status(folder, "(UIDNEXT)")
|
||||
.await
|
||||
.context("STATUS (UIDNEXT) error for {folder:?}")?;
|
||||
|
||||
if let Some(uid_next) = status.uid_next {
|
||||
uid_next
|
||||
} else {
|
||||
warn!(context, "STATUS {folder} (UIDNEXT) did not return UIDNEXT");
|
||||
|
||||
// Set UIDNEXT to 1 as a last resort fallback.
|
||||
1
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
set_uid_next(context, folder, new_uid_next).await?;
|
||||
set_uidvalidity(context, folder, new_uid_validity).await?;
|
||||
|
||||
@@ -867,14 +864,28 @@ impl Imap {
|
||||
uids_fetch_in_batch.push(uid);
|
||||
}
|
||||
|
||||
// determine which uid_next to use to update to
|
||||
// receive_imf() returns an `Err` value only on recoverable errors, otherwise it just logs an error.
|
||||
// `largest_uid_processed` is the largest uid where receive_imf() did NOT return an error.
|
||||
|
||||
// So: Update the uid_next to the largest uid that did NOT recoverably fail. Not perfect because if there was
|
||||
// another message afterwards that succeeded, we will not retry. The upside is that we will not retry an infinite amount of times.
|
||||
let largest_uid_without_errors = max(largest_uid_fetched, largest_uid_skipped.unwrap_or(0));
|
||||
let new_uid_next = largest_uid_without_errors + 1;
|
||||
// Advance uid_next to the maximum of the largest known UID plus 1
|
||||
// and mailbox UIDNEXT.
|
||||
// Largest known UID is normally less than UIDNEXT,
|
||||
// but a message may have arrived between determining UIDNEXT
|
||||
// and executing the FETCH command.
|
||||
let mailbox_uid_next = self
|
||||
.session
|
||||
.as_ref()
|
||||
.context("No IMAP session")?
|
||||
.selected_mailbox
|
||||
.as_ref()
|
||||
.with_context(|| format!("Expected {folder:?} to be selected"))?
|
||||
.uid_next
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Expected UIDNEXT to be determined for {folder:?} by select_with_uidvalidity"
|
||||
)
|
||||
})?;
|
||||
let new_uid_next = max(
|
||||
max(largest_uid_fetched, largest_uid_skipped.unwrap_or(0)) + 1,
|
||||
mailbox_uid_next,
|
||||
);
|
||||
|
||||
if new_uid_next > old_uid_next {
|
||||
set_uid_next(context, folder, new_uid_next).await?;
|
||||
|
||||
+34
-24
@@ -8,7 +8,7 @@ use futures_lite::FutureExt;
|
||||
use super::session::Session;
|
||||
use super::Imap;
|
||||
use crate::config::Config;
|
||||
use crate::imap::{client::IMAP_TIMEOUT, FolderMeaning};
|
||||
use crate::imap::{client::IMAP_TIMEOUT, get_uid_next, FolderMeaning};
|
||||
use crate::log::LogExt;
|
||||
use crate::{context::Context, scheduler::InterruptInfo};
|
||||
|
||||
@@ -19,7 +19,7 @@ impl Session {
|
||||
mut self,
|
||||
context: &Context,
|
||||
idle_interrupt_receiver: Receiver<InterruptInfo>,
|
||||
watch_folder: Option<String>,
|
||||
folder: &str,
|
||||
) -> Result<(Self, InterruptInfo)> {
|
||||
use futures::future::FutureExt;
|
||||
|
||||
@@ -33,12 +33,35 @@ impl Session {
|
||||
|
||||
let mut info = Default::default();
|
||||
|
||||
self.select_folder(context, watch_folder.as_deref()).await?;
|
||||
self.select_folder(context, Some(folder)).await?;
|
||||
|
||||
if self.server_sent_unsolicited_exists(context)? {
|
||||
return Ok((self, info));
|
||||
}
|
||||
|
||||
// Despite checking for unsolicited EXISTS above,
|
||||
// we may have missed EXISTS if the message was
|
||||
// received when the folder was not selected.
|
||||
let status = self
|
||||
.status(folder, "(UIDNEXT)")
|
||||
.await
|
||||
.context("STATUS (UIDNEXT) error for {folder:?}")?;
|
||||
if let Some(uid_next) = status.uid_next {
|
||||
let expected_uid_next = get_uid_next(context, folder)
|
||||
.await
|
||||
.with_context(|| format!("failed to get old UID NEXT for folder {folder}"))?;
|
||||
if uid_next > expected_uid_next {
|
||||
info!(
|
||||
context,
|
||||
"Skipping IDLE on {folder:?} because UIDNEXT {uid_next}>{expected_uid_next} indicates there are new messages."
|
||||
);
|
||||
return Ok((self, info));
|
||||
}
|
||||
} else {
|
||||
warn!(context, "STATUS {folder} (UIDNEXT) did not return UIDNEXT");
|
||||
// Go to IDLE anyway if STATUS is broken.
|
||||
}
|
||||
|
||||
if let Ok(info) = idle_interrupt_receiver.try_recv() {
|
||||
info!(context, "skip idle, got interrupt {:?}", info);
|
||||
return Ok((self, info));
|
||||
@@ -61,11 +84,7 @@ impl Session {
|
||||
Interrupt(InterruptInfo),
|
||||
}
|
||||
|
||||
let folder_name = watch_folder.as_deref().unwrap_or("None");
|
||||
info!(
|
||||
context,
|
||||
"{}: Idle entering wait-on-remote state", folder_name
|
||||
);
|
||||
info!(context, "{folder}: Idle entering wait-on-remote state");
|
||||
let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race(async {
|
||||
let info = idle_interrupt_receiver.recv().await;
|
||||
|
||||
@@ -77,36 +96,27 @@ impl Session {
|
||||
|
||||
match fut.await {
|
||||
Ok(Event::IdleResponse(IdleResponse::NewData(x))) => {
|
||||
info!(context, "{}: Idle has NewData {:?}", folder_name, x);
|
||||
info!(context, "{folder}: Idle has NewData {:?}", x);
|
||||
}
|
||||
Ok(Event::IdleResponse(IdleResponse::Timeout)) => {
|
||||
info!(
|
||||
context,
|
||||
"{}: Idle-wait timeout or interruption", folder_name
|
||||
);
|
||||
info!(context, "{folder}: Idle-wait timeout or interruption");
|
||||
}
|
||||
Ok(Event::IdleResponse(IdleResponse::ManualInterrupt)) => {
|
||||
info!(
|
||||
context,
|
||||
"{}: Idle wait was interrupted manually", folder_name
|
||||
);
|
||||
info!(context, "{folder}: Idle wait was interrupted manually");
|
||||
}
|
||||
Ok(Event::Interrupt(i)) => {
|
||||
info!(
|
||||
context,
|
||||
"{}: Idle wait was interrupted: {:?}", folder_name, &i
|
||||
);
|
||||
info!(context, "{folder}: Idle wait was interrupted: {:?}", &i);
|
||||
info = i;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "{}: Idle wait errored: {:?}", folder_name, err);
|
||||
warn!(context, "{folder}: Idle wait errored: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
let mut session = tokio::time::timeout(Duration::from_secs(15), handle.done())
|
||||
.await
|
||||
.with_context(|| format!("{folder_name}: IMAP IDLE protocol timed out"))?
|
||||
.with_context(|| format!("{folder_name}: IMAP IDLE failed"))?;
|
||||
.with_context(|| format!("{folder}: IMAP IDLE protocol timed out"))?
|
||||
.with_context(|| format!("{folder}: IMAP IDLE failed"))?;
|
||||
session.as_mut().set_read_timeout(Some(IMAP_TIMEOUT));
|
||||
self.inner = session;
|
||||
|
||||
|
||||
+9
-1
@@ -1818,6 +1818,14 @@ pub async fn estimate_deletion_cnt(
|
||||
pub(crate) async fn rfc724_mid_exists(
|
||||
context: &Context,
|
||||
rfc724_mid: &str,
|
||||
) -> Result<Option<MsgId>> {
|
||||
rfc724_mid_exists_and(context, rfc724_mid, "1").await
|
||||
}
|
||||
|
||||
pub(crate) async fn rfc724_mid_exists_and(
|
||||
context: &Context,
|
||||
rfc724_mid: &str,
|
||||
cond: &str,
|
||||
) -> Result<Option<MsgId>> {
|
||||
let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>');
|
||||
if rfc724_mid.is_empty() {
|
||||
@@ -1828,7 +1836,7 @@ pub(crate) async fn rfc724_mid_exists(
|
||||
let res = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT id FROM msgs WHERE rfc724_mid=?",
|
||||
&("SELECT id FROM msgs WHERE rfc724_mid=? AND ".to_string() + cond),
|
||||
(rfc724_mid,),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
|
||||
+7
-1
@@ -59,7 +59,7 @@ pub(crate) struct MimeMessage {
|
||||
/// Message headers.
|
||||
headers: HashMap<String, String>,
|
||||
|
||||
/// Addresses are normalized and lowercased:
|
||||
/// Addresses are normalized and lowercase
|
||||
pub recipients: Vec<SingleInfo>,
|
||||
|
||||
/// `From:` address.
|
||||
@@ -376,6 +376,12 @@ impl MimeMessage {
|
||||
if !encrypted {
|
||||
signatures.clear();
|
||||
}
|
||||
if let Some(peerstate) = &mut decryption_info.peerstate {
|
||||
if peerstate.prefer_encrypt != EncryptPreference::Mutual && !signatures.is_empty() {
|
||||
peerstate.prefer_encrypt = EncryptPreference::Mutual;
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-submitted is also set by holiday-notices so we also check `chat-version`
|
||||
let is_bot = headers.contains_key("auto-submitted") && headers.contains_key("chat-version");
|
||||
|
||||
+21
-2
@@ -1,10 +1,11 @@
|
||||
//! # Common network utilities.
|
||||
use std::net::Ipv4Addr;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::pin::Pin;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Error, Result};
|
||||
use anyhow::{format_err, Context as _, Result};
|
||||
use tokio::net::{lookup_host, TcpStream};
|
||||
use tokio::time::timeout;
|
||||
use tokio_io_timeout::TimeoutStream;
|
||||
@@ -119,6 +120,22 @@ async fn lookup_host_with_cache(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if resolved_addrs.is_empty() {
|
||||
// Load hardcoded cache if everything else fails.
|
||||
//
|
||||
// See <https://support.delta.chat/t/no-dns-resolution-result/2778> and
|
||||
// <https://github.com/deltachat/deltachat-core-rust/issues/4920> for reasons.
|
||||
//
|
||||
// In the future we may pre-resolve all provider database addresses
|
||||
// and build them in.
|
||||
if hostname == "mail.sangham.net" {
|
||||
resolved_addrs.push(SocketAddr::new(
|
||||
IpAddr::V4(Ipv4Addr::new(159, 69, 186, 85)),
|
||||
port,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(resolved_addrs)
|
||||
@@ -178,7 +195,9 @@ pub(crate) async fn connect_tcp(
|
||||
let tcp_stream = match tcp_stream {
|
||||
Some(tcp_stream) => tcp_stream,
|
||||
None => {
|
||||
return Err(last_error.unwrap_or_else(|| Error::msg("no DNS resolution results")));
|
||||
return Err(
|
||||
last_error.unwrap_or_else(|| format_err!("no DNS resolution results for {host}"))
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+27
-32
@@ -427,42 +427,37 @@ impl Peerstate {
|
||||
&mut self,
|
||||
which_key: PeerstateKeyType,
|
||||
fingerprint: Fingerprint,
|
||||
verified: PeerstateVerifiedStatus,
|
||||
verifier: String,
|
||||
) -> Result<()> {
|
||||
if verified == PeerstateVerifiedStatus::BidirectVerified {
|
||||
match which_key {
|
||||
PeerstateKeyType::PublicKey => {
|
||||
if self.public_key_fingerprint.is_some()
|
||||
&& self.public_key_fingerprint.as_ref().unwrap() == &fingerprint
|
||||
{
|
||||
self.verified_key = self.public_key.clone();
|
||||
self.verified_key_fingerprint = Some(fingerprint);
|
||||
self.verifier = Some(verifier);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::msg(format!(
|
||||
"{fingerprint} is not peer's public key fingerprint",
|
||||
)))
|
||||
}
|
||||
}
|
||||
PeerstateKeyType::GossipKey => {
|
||||
if self.gossip_key_fingerprint.is_some()
|
||||
&& self.gossip_key_fingerprint.as_ref().unwrap() == &fingerprint
|
||||
{
|
||||
self.verified_key = self.gossip_key.clone();
|
||||
self.verified_key_fingerprint = Some(fingerprint);
|
||||
self.verifier = Some(verifier);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::msg(format!(
|
||||
"{fingerprint} is not peer's gossip key fingerprint",
|
||||
)))
|
||||
}
|
||||
match which_key {
|
||||
PeerstateKeyType::PublicKey => {
|
||||
if self.public_key_fingerprint.is_some()
|
||||
&& self.public_key_fingerprint.as_ref().unwrap() == &fingerprint
|
||||
{
|
||||
self.verified_key = self.public_key.clone();
|
||||
self.verified_key_fingerprint = Some(fingerprint);
|
||||
self.verifier = Some(verifier);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::msg(format!(
|
||||
"{fingerprint} is not peer's public key fingerprint",
|
||||
)))
|
||||
}
|
||||
}
|
||||
PeerstateKeyType::GossipKey => {
|
||||
if self.gossip_key_fingerprint.is_some()
|
||||
&& self.gossip_key_fingerprint.as_ref().unwrap() == &fingerprint
|
||||
{
|
||||
self.verified_key = self.gossip_key.clone();
|
||||
self.verified_key_fingerprint = Some(fingerprint);
|
||||
self.verifier = Some(verifier);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::msg(format!(
|
||||
"{fingerprint} is not peer's gossip key fingerprint",
|
||||
)))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Err(Error::msg("BidirectVerified required"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+133
-1
@@ -222,6 +222,99 @@ static P_BUZON_UY: Provider = Provider {
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// c1.testrun.org.md: c1.testrun.org
|
||||
static P_C1_TESTRUN_ORG: Provider = Provider {
|
||||
id: "c1.testrun.org",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/c1-testrun-org",
|
||||
server: &[
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
hostname: "c1.testrun.org",
|
||||
port: 993,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Ssl,
|
||||
hostname: "c1.testrun.org",
|
||||
port: 465,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: Some(&[ConfigDefault {
|
||||
key: Config::MvboxMove,
|
||||
value: "0",
|
||||
}]),
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// c2.testrun.org.md: c2.testrun.org
|
||||
static P_C2_TESTRUN_ORG: Provider = Provider {
|
||||
id: "c2.testrun.org",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/c2-testrun-org",
|
||||
server: &[
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
hostname: "c2.testrun.org",
|
||||
port: 993,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Ssl,
|
||||
hostname: "c2.testrun.org",
|
||||
port: 465,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: Some(&[ConfigDefault {
|
||||
key: Config::MvboxMove,
|
||||
value: "0",
|
||||
}]),
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// c3.testrun.org.md: c3.testrun.org
|
||||
static P_C3_TESTRUN_ORG: Provider = Provider {
|
||||
id: "c3.testrun.org",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/c3-testrun-org",
|
||||
server: &[
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
hostname: "c3.testrun.org",
|
||||
port: 993,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Ssl,
|
||||
hostname: "c3.testrun.org",
|
||||
port: 465,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: Some(&[ConfigDefault {
|
||||
key: Config::MvboxMove,
|
||||
value: "0",
|
||||
}]),
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// chello.at.md: chello.at
|
||||
static P_CHELLO_AT: Provider = Provider {
|
||||
id: "chello.at",
|
||||
@@ -867,6 +960,37 @@ static P_NAVER: Provider = Provider {
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// nine.testrun.org.md: nine.testrun.org
|
||||
static P_NINE_TESTRUN_ORG: Provider = Provider {
|
||||
id: "nine.testrun.org",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/nine-testrun-org",
|
||||
server: &[
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
hostname: "nine.testrun.org",
|
||||
port: 993,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Ssl,
|
||||
hostname: "nine.testrun.org",
|
||||
port: 465,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: Some(&[ConfigDefault {
|
||||
key: Config::MvboxMove,
|
||||
value: "0",
|
||||
}]),
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// nubo.coop.md: nubo.coop
|
||||
static P_NUBO_COOP: Provider = Provider {
|
||||
id: "nubo.coop",
|
||||
@@ -1495,6 +1619,9 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
|
||||
("delta.blindzeln.org", &P_BLINDZELN_ORG),
|
||||
("bluewin.ch", &P_BLUEWIN_CH),
|
||||
("buzon.uy", &P_BUZON_UY),
|
||||
("c1.testrun.org", &P_C1_TESTRUN_ORG),
|
||||
("c2.testrun.org", &P_C2_TESTRUN_ORG),
|
||||
("c3.testrun.org", &P_C3_TESTRUN_ORG),
|
||||
("chello.at", &P_CHELLO_AT),
|
||||
("xfinity.com", &P_COMCAST),
|
||||
("comcast.net", &P_COMCAST),
|
||||
@@ -1708,6 +1835,7 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
|
||||
("mailo.com", &P_MAILO_COM),
|
||||
("nauta.cu", &P_NAUTA_CU),
|
||||
("naver.com", &P_NAVER),
|
||||
("nine.testrun.org", &P_NINE_TESTRUN_ORG),
|
||||
("nubo.coop", &P_NUBO_COOP),
|
||||
("hotmail.com", &P_OUTLOOK_COM),
|
||||
("outlook.com", &P_OUTLOOK_COM),
|
||||
@@ -1860,6 +1988,9 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
|
||||
("blindzeln.org", &P_BLINDZELN_ORG),
|
||||
("bluewin.ch", &P_BLUEWIN_CH),
|
||||
("buzon.uy", &P_BUZON_UY),
|
||||
("c1.testrun.org", &P_C1_TESTRUN_ORG),
|
||||
("c2.testrun.org", &P_C2_TESTRUN_ORG),
|
||||
("c3.testrun.org", &P_C3_TESTRUN_ORG),
|
||||
("chello.at", &P_CHELLO_AT),
|
||||
("comcast", &P_COMCAST),
|
||||
("dismail.de", &P_DISMAIL_DE),
|
||||
@@ -1888,6 +2019,7 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
|
||||
("mailo.com", &P_MAILO_COM),
|
||||
("nauta.cu", &P_NAUTA_CU),
|
||||
("naver", &P_NAVER),
|
||||
("nine.testrun.org", &P_NINE_TESTRUN_ORG),
|
||||
("nubo.coop", &P_NUBO_COOP),
|
||||
("outlook.com", &P_OUTLOOK_COM),
|
||||
("ouvaton.coop", &P_OUVATON_COOP),
|
||||
@@ -1918,4 +2050,4 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
|
||||
});
|
||||
|
||||
pub static _PROVIDER_UPDATED: Lazy<chrono::NaiveDate> =
|
||||
Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2023, 2, 20).unwrap());
|
||||
Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2023, 11, 5).unwrap());
|
||||
|
||||
+111
-101
@@ -26,16 +26,18 @@ use crate::imap::{markseen_on_imap_table, GENERATED_PREFIX};
|
||||
use crate::location;
|
||||
use crate::log::LogExt;
|
||||
use crate::message::{
|
||||
self, rfc724_mid_exists, Message, MessageState, MessengerMessage, MsgId, Viewtype,
|
||||
self, rfc724_mid_exists, rfc724_mid_exists_and, Message, MessageState, MessengerMessage, MsgId,
|
||||
Viewtype,
|
||||
};
|
||||
use crate::mimeparser::{parse_message_ids, AvatarAction, MimeMessage, SystemMessage};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus};
|
||||
use crate::peerstate::{Peerstate, PeerstateKeyType};
|
||||
use crate::reaction::{set_msg_reaction, Reaction};
|
||||
use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
|
||||
use crate::simplify;
|
||||
use crate::sql;
|
||||
use crate::stock_str;
|
||||
use crate::sync::Sync::*;
|
||||
use crate::tools::{
|
||||
buf_compress, extract_grpid_from_rfc724_mid, smeared_time, strip_rtlo_characters,
|
||||
};
|
||||
@@ -455,6 +457,7 @@ async fn add_parts(
|
||||
let mut chat_id_blocked = Blocked::Not;
|
||||
|
||||
let mut better_msg = None;
|
||||
let mut group_changes_msgs = Vec::new();
|
||||
if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled {
|
||||
better_msg = Some(stock_str::msg_location_enabled_by(context, from_id).await);
|
||||
}
|
||||
@@ -502,7 +505,6 @@ async fn add_parts(
|
||||
// - incoming messages introduce a chat only for known contacts if they are sent by a messenger
|
||||
// (of course, the user can add other chats manually later)
|
||||
let to_id: ContactId;
|
||||
|
||||
let state: MessageState;
|
||||
let mut needs_delete_job = false;
|
||||
if incoming {
|
||||
@@ -514,25 +516,23 @@ async fn add_parts(
|
||||
|
||||
// handshake may mark contacts as verified and must be processed before chats are created
|
||||
if mime_parser.get_header(HeaderDef::SecureJoin).is_some() {
|
||||
match handle_securejoin_handshake(context, mime_parser, from_id).await {
|
||||
Ok(securejoin::HandshakeMessage::Done) => {
|
||||
match handle_securejoin_handshake(context, mime_parser, from_id)
|
||||
.await
|
||||
.context("error in Secure-Join message handling")?
|
||||
{
|
||||
securejoin::HandshakeMessage::Done => {
|
||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||
needs_delete_job = true;
|
||||
securejoin_seen = true;
|
||||
}
|
||||
Ok(securejoin::HandshakeMessage::Ignore) => {
|
||||
securejoin::HandshakeMessage::Ignore => {
|
||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||
securejoin_seen = true;
|
||||
}
|
||||
Ok(securejoin::HandshakeMessage::Propagate) => {
|
||||
securejoin::HandshakeMessage::Propagate => {
|
||||
// process messages as "member added" normally
|
||||
securejoin_seen = false;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Error in Secure-Join message handling: {err:#}.");
|
||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||
securejoin_seen = true;
|
||||
}
|
||||
}
|
||||
// Peerstate could be updated by handling the Securejoin handshake.
|
||||
let contact = Contact::get_by_id(context, from_id).await?;
|
||||
@@ -650,15 +650,16 @@ async fn add_parts(
|
||||
}
|
||||
}
|
||||
|
||||
better_msg = better_msg.or(apply_group_changes(
|
||||
group_changes_msgs = apply_group_changes(
|
||||
context,
|
||||
mime_parser,
|
||||
sent_timestamp,
|
||||
group_chat_id,
|
||||
from_id,
|
||||
to_ids,
|
||||
is_partial_download.is_some(),
|
||||
)
|
||||
.await?);
|
||||
.await?;
|
||||
}
|
||||
|
||||
if chat_id.is_none() {
|
||||
@@ -808,19 +809,17 @@ async fn add_parts(
|
||||
|
||||
// handshake may mark contacts as verified and must be processed before chats are created
|
||||
if mime_parser.get_header(HeaderDef::SecureJoin).is_some() {
|
||||
match observe_securejoin_on_other_device(context, mime_parser, to_id).await {
|
||||
Ok(securejoin::HandshakeMessage::Done)
|
||||
| Ok(securejoin::HandshakeMessage::Ignore) => {
|
||||
match observe_securejoin_on_other_device(context, mime_parser, to_id)
|
||||
.await
|
||||
.context("error in Secure-Join watching")?
|
||||
{
|
||||
securejoin::HandshakeMessage::Done | securejoin::HandshakeMessage::Ignore => {
|
||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||
}
|
||||
Ok(securejoin::HandshakeMessage::Propagate) => {
|
||||
securejoin::HandshakeMessage::Propagate => {
|
||||
// process messages as "member added" normally
|
||||
chat_id = None;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Error in Secure-Join watching: {err:#}.");
|
||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||
}
|
||||
}
|
||||
} else if mime_parser.sync_items.is_some() && self_sent {
|
||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||
@@ -887,22 +886,23 @@ async fn add_parts(
|
||||
// automatically unblock chat when the user sends a message
|
||||
if chat_id_blocked != Blocked::Not {
|
||||
if let Some(chat_id) = chat_id {
|
||||
chat_id.unblock(context).await?;
|
||||
chat_id.unblock_ex(context, Nosync).await?;
|
||||
chat_id_blocked = Blocked::Not;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(chat_id) = chat_id {
|
||||
better_msg = better_msg.or(apply_group_changes(
|
||||
group_changes_msgs = apply_group_changes(
|
||||
context,
|
||||
mime_parser,
|
||||
sent_timestamp,
|
||||
chat_id,
|
||||
from_id,
|
||||
to_ids,
|
||||
is_partial_download.is_some(),
|
||||
)
|
||||
.await?);
|
||||
.await?;
|
||||
}
|
||||
|
||||
if chat_id.is_none() && self_sent {
|
||||
@@ -919,7 +919,7 @@ async fn add_parts(
|
||||
|
||||
if let Some(chat_id) = chat_id {
|
||||
if Blocked::Not != chat_id_blocked {
|
||||
chat_id.unblock(context).await?;
|
||||
chat_id.unblock_ex(context, Nosync).await?;
|
||||
// Not assigning `chat_id_blocked = Blocked::Not` to avoid unused_assignments warning.
|
||||
}
|
||||
}
|
||||
@@ -1116,7 +1116,9 @@ async fn add_parts(
|
||||
|
||||
let mut created_db_entries = Vec::with_capacity(mime_parser.parts.len());
|
||||
|
||||
for part in &mut mime_parser.parts {
|
||||
group_changes_msgs = group_changes_msgs.into_iter().rev().collect();
|
||||
let mut parts = mime_parser.parts.iter_mut().peekable();
|
||||
while let Some(part) = parts.peek() {
|
||||
if part.is_reaction {
|
||||
let reaction_str = simplify::remove_footers(part.msg.as_str());
|
||||
set_msg_reaction(
|
||||
@@ -1149,7 +1151,10 @@ async fn add_parts(
|
||||
}
|
||||
|
||||
let mut txt_raw = "".to_string();
|
||||
let (msg, typ): (&str, Viewtype) = if let Some(better_msg) = &better_msg {
|
||||
let group_changes_msg = group_changes_msgs.pop();
|
||||
let (msg, typ): (&str, Viewtype) = if let Some(msg) = &group_changes_msg {
|
||||
(msg, Viewtype::Text)
|
||||
} else if let Some(better_msg) = &better_msg {
|
||||
(better_msg, Viewtype::Text)
|
||||
} else {
|
||||
(&part.msg, part.typ)
|
||||
@@ -1277,6 +1282,10 @@ RETURNING id
|
||||
|
||||
debug_assert!(!row_id.is_special());
|
||||
created_db_entries.push(row_id);
|
||||
|
||||
if group_changes_msg.is_none() || (group_changes_msgs.is_empty() && better_msg.is_none()) {
|
||||
parts.next();
|
||||
}
|
||||
}
|
||||
|
||||
// check all parts whether they contain a new logging webxdc
|
||||
@@ -1657,6 +1666,7 @@ async fn create_or_lookup_group(
|
||||
members.push(from_id);
|
||||
}
|
||||
members.extend(to_ids);
|
||||
members.sort_unstable();
|
||||
members.dedup();
|
||||
chat::add_to_chat_contacts_table(context, new_chat_id, &members).await?;
|
||||
|
||||
@@ -1695,6 +1705,7 @@ async fn create_or_lookup_group(
|
||||
/// Apply group member list, name, avatar and protection status changes from the MIME message.
|
||||
///
|
||||
/// Optionally returns better message to replace the original system message.
|
||||
/// is_partial_download: whether the message is not fully downloaded.
|
||||
async fn apply_group_changes(
|
||||
context: &Context,
|
||||
mime_parser: &mut MimeMessage,
|
||||
@@ -1702,15 +1713,16 @@ async fn apply_group_changes(
|
||||
chat_id: ChatId,
|
||||
from_id: ContactId,
|
||||
to_ids: &[ContactId],
|
||||
) -> Result<Option<String>> {
|
||||
is_partial_download: bool,
|
||||
) -> Result<Vec<String>> {
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
if chat.typ != Chattype::Group {
|
||||
return Ok(None);
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut send_event_chat_modified = false;
|
||||
let (mut removed_id, mut added_id) = (None, None);
|
||||
let mut better_msg = None;
|
||||
let mut group_changes_msgs = Vec::new();
|
||||
|
||||
// True if a Delta Chat client has explicitly added our current primary address.
|
||||
let self_added =
|
||||
@@ -1720,12 +1732,14 @@ async fn apply_group_changes(
|
||||
false
|
||||
};
|
||||
|
||||
let mut chat_contacts = HashSet::from_iter(chat::get_chat_contacts(context, chat_id).await?);
|
||||
let mut chat_contacts =
|
||||
HashSet::<ContactId>::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
|
||||
let allow_member_list_changes = !is_partial_download
|
||||
&& is_from_in_chat
|
||||
&& chat_id
|
||||
.update_timestamp(context, Param::MemberListTimestamp, sent_timestamp)
|
||||
.await?;
|
||||
@@ -1739,7 +1753,9 @@ async fn apply_group_changes(
|
||||
|| 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(),
|
||||
Some(reply_to) => rfc724_mid_exists_and(context, reply_to, "download_state=0")
|
||||
.await?
|
||||
.is_none(),
|
||||
None => false,
|
||||
}
|
||||
} && {
|
||||
@@ -1771,11 +1787,11 @@ async fn apply_group_changes(
|
||||
if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
|
||||
removed_id = Contact::lookup_id_by_addr(context, removed_addr, Origin::Unknown).await?;
|
||||
|
||||
better_msg = if removed_id == Some(from_id) {
|
||||
Some(stock_str::msg_group_left_local(context, from_id).await)
|
||||
group_changes_msgs.push(if removed_id == Some(from_id) {
|
||||
stock_str::msg_group_left_local(context, from_id).await
|
||||
} else {
|
||||
Some(stock_str::msg_del_member_local(context, removed_addr, from_id).await)
|
||||
};
|
||||
stock_str::msg_del_member_local(context, removed_addr, from_id).await
|
||||
});
|
||||
|
||||
if removed_id.is_some() {
|
||||
if !allow_member_list_changes {
|
||||
@@ -1788,7 +1804,8 @@ async fn apply_group_changes(
|
||||
warn!(context, "Removed {removed_addr:?} has no contact id.")
|
||||
}
|
||||
} else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
|
||||
better_msg = Some(stock_str::msg_add_member_local(context, added_addr, from_id).await);
|
||||
group_changes_msgs
|
||||
.push(stock_str::msg_add_member_local(context, added_addr, from_id).await);
|
||||
|
||||
if allow_member_list_changes {
|
||||
if !recreate_member_list {
|
||||
@@ -1829,21 +1846,20 @@ async fn apply_group_changes(
|
||||
send_event_chat_modified = true;
|
||||
}
|
||||
|
||||
better_msg = Some(stock_str::msg_grp_name(context, old_name, grpname, from_id).await);
|
||||
group_changes_msgs
|
||||
.push(stock_str::msg_grp_name(context, old_name, grpname, from_id).await);
|
||||
}
|
||||
} else if let Some(value) = mime_parser.get_header(HeaderDef::ChatContent) {
|
||||
if value == "group-avatar-changed" {
|
||||
if let Some(avatar_action) = &mime_parser.group_avatar {
|
||||
// this is just an explicit message containing the group-avatar,
|
||||
// apart from that, the group-avatar is send along with various other messages
|
||||
better_msg = match avatar_action {
|
||||
AvatarAction::Delete => {
|
||||
Some(stock_str::msg_grp_img_deleted(context, from_id).await)
|
||||
}
|
||||
group_changes_msgs.push(match avatar_action {
|
||||
AvatarAction::Delete => stock_str::msg_grp_img_deleted(context, from_id).await,
|
||||
AvatarAction::Change(_) => {
|
||||
Some(stock_str::msg_grp_img_changed(context, from_id).await)
|
||||
stock_str::msg_grp_img_changed(context, from_id).await
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1856,33 +1872,43 @@ async fn apply_group_changes(
|
||||
}
|
||||
|
||||
if !recreate_member_list {
|
||||
let diff: HashSet<ContactId> =
|
||||
chat_contacts.difference(&new_members).copied().collect();
|
||||
// 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);
|
||||
// Don't delete any members locally, but instead add absent ones to provide group
|
||||
// membership consistency for all members:
|
||||
// - 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.
|
||||
// - DC users could miss new member additions and then better to handle this in the same
|
||||
// way as for classical MUA messages. Moreover, if we remove a member implicitly, they
|
||||
// will never know that and continue to think they're still here.
|
||||
// But it shouldn't be a big problem if somebody missed a member removal, 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.
|
||||
let mut diff: HashSet<ContactId> =
|
||||
new_members.difference(&chat_contacts).copied().collect();
|
||||
new_members = chat_contacts.clone();
|
||||
new_members.extend(diff.clone());
|
||||
if let Some(added_id) = added_id {
|
||||
diff.remove(&added_id);
|
||||
}
|
||||
if !diff.is_empty() {
|
||||
warn!(context, "Implicit addition of {diff:?} to chat {chat_id}.");
|
||||
}
|
||||
group_changes_msgs.reserve(diff.len());
|
||||
for contact_id in diff {
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
group_changes_msgs.push(
|
||||
stock_str::msg_add_member_local(
|
||||
context,
|
||||
contact.get_addr(),
|
||||
ContactId::UNDEFINED,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
}
|
||||
}
|
||||
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 {
|
||||
info!(
|
||||
context,
|
||||
@@ -1945,7 +1971,7 @@ async fn apply_group_changes(
|
||||
if send_event_chat_modified {
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
}
|
||||
Ok(better_msg)
|
||||
Ok(group_changes_msgs)
|
||||
}
|
||||
|
||||
static LIST_ID_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
|
||||
@@ -2326,47 +2352,31 @@ async fn has_verified_encryption(
|
||||
|
||||
let contact = Contact::get_by_id(context, from_id).await?;
|
||||
|
||||
for (to_addr, mut is_verified) in rows {
|
||||
info!(
|
||||
context,
|
||||
"has_verified_encryption: {:?} self={:?}.",
|
||||
to_addr,
|
||||
context.is_self_addr(&to_addr).await
|
||||
);
|
||||
let peerstate = Peerstate::from_addr(context, &to_addr).await?;
|
||||
|
||||
for (to_addr, is_verified) in rows {
|
||||
// mark gossiped keys (if any) as verified
|
||||
if mimeparser.gossiped_addr.contains(&to_addr.to_lowercase()) {
|
||||
if let Some(mut peerstate) = peerstate {
|
||||
// if we're here, we know the gossip key is verified:
|
||||
// - use the gossip-key as verified-key if there is no verified-key
|
||||
// - OR if the verified-key does not match public-key or gossip-key
|
||||
// (otherwise a verified key can _only_ be updated through QR scan which might be annoying,
|
||||
// see <https://github.com/nextleap-project/countermitm/issues/46> for a discussion about this point)
|
||||
if let Some(mut peerstate) = Peerstate::from_addr(context, &to_addr).await? {
|
||||
// If we're here, we know the gossip key is verified.
|
||||
// Use the gossip-key as verified-key if there is no verified-key
|
||||
// or a member is reintroduced to a verified group.
|
||||
//
|
||||
// See <https://github.com/nextleap-project/countermitm/issues/46>
|
||||
// and <https://github.com/deltachat/deltachat-core-rust/issues/4541> for discussion.
|
||||
let verifier_addr = contact.get_addr().to_owned();
|
||||
if !is_verified
|
||||
|| peerstate.verified_key_fingerprint != peerstate.public_key_fingerprint
|
||||
&& peerstate.verified_key_fingerprint != peerstate.gossip_key_fingerprint
|
||||
|| mimeparser
|
||||
.get_header(HeaderDef::ChatGroupMemberAdded)
|
||||
.filter(|s| s.as_str() == to_addr)
|
||||
.is_some()
|
||||
{
|
||||
info!(context, "{} has verified {}.", contact.get_addr(), to_addr);
|
||||
let fp = peerstate.gossip_key_fingerprint.clone();
|
||||
if let Some(fp) = fp {
|
||||
peerstate.set_verified(
|
||||
PeerstateKeyType::GossipKey,
|
||||
fp,
|
||||
PeerstateVerifiedStatus::BidirectVerified,
|
||||
contact.get_addr().to_owned(),
|
||||
)?;
|
||||
info!(context, "{verifier_addr} has verified {to_addr}.");
|
||||
if let Some(fp) = peerstate.gossip_key_fingerprint.clone() {
|
||||
peerstate.set_verified(PeerstateKeyType::GossipKey, fp, verifier_addr)?;
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
is_verified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !is_verified {
|
||||
return Ok(NotVerified(format!(
|
||||
"{to_addr} is not a member of this protected chat member list {to_ids:?}",
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(Verified)
|
||||
}
|
||||
|
||||
+169
-6
@@ -3117,6 +3117,46 @@ async fn test_thunderbird_autocrypt() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_prefer_encrypt_mutual_if_encrypted() -> Result<()> {
|
||||
let t = TestContext::new_bob().await;
|
||||
|
||||
let raw =
|
||||
include_bytes!("../../test-data/message/thunderbird_encrypted_signed_with_pubkey.eml");
|
||||
receive_imf(&t, raw, false).await?;
|
||||
let peerstate = Peerstate::from_addr(&t, "alice@example.org")
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
receive_imf(
|
||||
&t,
|
||||
b"From: alice@example.org\n\
|
||||
To: bob@example.net\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <message@example.org>\n\
|
||||
Date: Thu, 2 Nov 2023 02:20:28 -0300\n\
|
||||
\n\
|
||||
unencrypted\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let peerstate = Peerstate::from_addr(&t, "alice@example.org")
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Reset);
|
||||
|
||||
let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_signed.eml");
|
||||
receive_imf(&t, raw, false).await?;
|
||||
let peerstate = Peerstate::from_addr(&t, "alice@example.org")
|
||||
.await?
|
||||
.unwrap();
|
||||
assert!(peerstate.public_key.is_some());
|
||||
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_thunderbird_autocrypt_unencrypted() -> Result<()> {
|
||||
let t = TestContext::new_bob().await;
|
||||
@@ -3371,14 +3411,24 @@ 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 mustn't remove Fiona from the members
|
||||
// list back. Instead, Bob must add Fiona from the next Alice's message to make their group
|
||||
// members view consistent.
|
||||
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.
|
||||
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);
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 3);
|
||||
|
||||
send_text_msg(
|
||||
&alice,
|
||||
alice_chat_id,
|
||||
"Finally add Fiona please".to_string(),
|
||||
)
|
||||
.await?;
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 3);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -3462,8 +3512,11 @@ async fn test_dont_readd_with_normal_msg() -> Result<()> {
|
||||
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
|
||||
// Alice didn't receive Bobs leave message, but bob shouldn't readded himself just because of that.
|
||||
assert!(!is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
|
||||
// Alice didn't receive Bob's leave message, so Bob must readd themselves otherwise other
|
||||
// members would think Bob is still here while they aren't, and then retry to leave if they
|
||||
// think that Alice didn't re-add them on purpose (which is possible if Alice uses a classical
|
||||
// MUA).
|
||||
assert!(is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3810,3 +3863,113 @@ async fn test_create_group_with_big_msg() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_partial_group_consistency() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob_id = Contact::create(&alice, "", "bob@example.net").await?;
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foos").await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
|
||||
|
||||
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
|
||||
let add = alice.pop_sent_msg().await;
|
||||
let bob = tcm.bob().await;
|
||||
bob.recv_msg(&add).await;
|
||||
let bob_chat_id = bob.get_last_msg().await.chat_id;
|
||||
let contacts = get_chat_contacts(&bob, bob_chat_id).await?;
|
||||
assert_eq!(contacts.len(), 2);
|
||||
|
||||
// Get initial timestamp.
|
||||
let timestamp = bob_chat_id
|
||||
.get_param(&bob)
|
||||
.await?
|
||||
.get_i64(Param::MemberListTimestamp)
|
||||
.unwrap();
|
||||
|
||||
// Bob receives partial message.
|
||||
let msg_id = receive_imf_inner(
|
||||
&bob,
|
||||
"first@example.org",
|
||||
b"From: Alice <alice@example.org>\n\
|
||||
To: <bob@example.net>, <charlie@example.com>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: subject\n\
|
||||
Message-ID: <first@example.org>\n\
|
||||
Date: Sun, 14 Nov 2021 00:10:00 +0000\
|
||||
Content-Type: text/plain
|
||||
Chat-Group-Member-Added: charlie@example.com",
|
||||
false,
|
||||
Some(100000),
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.context("no received message")?;
|
||||
|
||||
let msg = Message::load_from_db(&bob, msg_id.msg_ids[0]).await?;
|
||||
let timestamp2 = bob_chat_id
|
||||
.get_param(&bob)
|
||||
.await?
|
||||
.get_i64(Param::MemberListTimestamp)
|
||||
.unwrap();
|
||||
|
||||
// Partial download does not change the member list.
|
||||
assert_eq!(msg.download_state, DownloadState::Available);
|
||||
assert_eq!(timestamp, timestamp2);
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?, contacts);
|
||||
|
||||
// Alice sends normal message to bob, adding fiona.
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_chat_id,
|
||||
Contact::create(&alice, "fiona", "fiona@example.net").await?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
|
||||
let timestamp3 = bob_chat_id
|
||||
.get_param(&bob)
|
||||
.await?
|
||||
.get_i64(Param::MemberListTimestamp)
|
||||
.unwrap();
|
||||
|
||||
// Receiving a message after a partial download recreates the member list because we treat
|
||||
// such messages as if we have not seen them.
|
||||
assert_ne!(timestamp, timestamp3);
|
||||
let contacts = get_chat_contacts(&bob, bob_chat_id).await?;
|
||||
assert_eq!(contacts.len(), 3);
|
||||
|
||||
// Bob fully reives the partial message.
|
||||
let msg_id = receive_imf_inner(
|
||||
&bob,
|
||||
"first@example.org",
|
||||
b"From: Alice <alice@example.org>\n\
|
||||
To: Bob <bob@example.net>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: subject\n\
|
||||
Message-ID: <first@example.org>\n\
|
||||
Date: Sun, 14 Nov 2021 00:10:00 +0000\
|
||||
Content-Type: text/plain
|
||||
Chat-Group-Member-Added: charlie@example.com",
|
||||
false,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.context("no received message")?;
|
||||
|
||||
let msg = Message::load_from_db(&bob, msg_id.msg_ids[0]).await?;
|
||||
let timestamp4 = bob_chat_id
|
||||
.get_param(&bob)
|
||||
.await?
|
||||
.get_i64(Param::MemberListTimestamp)
|
||||
.unwrap();
|
||||
|
||||
// After full download, the old message should not change group state.
|
||||
assert_eq!(msg.download_state, DownloadState::Done);
|
||||
assert_eq!(timestamp3, timestamp4);
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?, contacts);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+1
-1
@@ -620,7 +620,7 @@ async fn fetch_idle(
|
||||
.idle(
|
||||
ctx,
|
||||
connection.idle_interrupt_receiver.clone(),
|
||||
Some(watch_folder),
|
||||
&watch_folder,
|
||||
)
|
||||
.await
|
||||
.context("idle")
|
||||
|
||||
+81
-109
@@ -8,8 +8,8 @@ use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Blocked, Chattype};
|
||||
use crate::contact::{Contact, ContactId, Origin, VerifiedStatus};
|
||||
use crate::constants::Blocked;
|
||||
use crate::contact::{Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::e2ee::ensure_secret_key_exists;
|
||||
use crate::events::EventType;
|
||||
@@ -18,7 +18,7 @@ use crate::key::{load_self_public_key, DcKey, Fingerprint};
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::param::Param;
|
||||
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus};
|
||||
use crate::peerstate::{Peerstate, PeerstateKeyType};
|
||||
use crate::qr::check_qr;
|
||||
use crate::stock_str;
|
||||
use crate::token;
|
||||
@@ -36,17 +36,15 @@ use crate::token::Namespace;
|
||||
/// Set of characters to percent-encode in email addresses and names.
|
||||
pub const NON_ALPHANUMERIC_WITHOUT_DOT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'.');
|
||||
|
||||
macro_rules! inviter_progress {
|
||||
($context:tt, $contact_id:expr, $progress:expr) => {
|
||||
assert!(
|
||||
$progress >= 0 && $progress <= 1000,
|
||||
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
|
||||
);
|
||||
$context.emit_event($crate::events::EventType::SecurejoinInviterProgress {
|
||||
contact_id: $contact_id,
|
||||
progress: $progress,
|
||||
});
|
||||
};
|
||||
fn inviter_progress(context: &Context, contact_id: ContactId, progress: usize) {
|
||||
debug_assert!(
|
||||
progress <= 1000,
|
||||
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
|
||||
);
|
||||
context.emit_event(EventType::SecurejoinInviterProgress {
|
||||
contact_id,
|
||||
progress,
|
||||
});
|
||||
}
|
||||
|
||||
/// Generates a Secure Join QR code.
|
||||
@@ -318,7 +316,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
}
|
||||
info!(context, "Secure-join requested.",);
|
||||
|
||||
inviter_progress!(context, contact_id, 300);
|
||||
inviter_progress(context, contact_id, 300);
|
||||
|
||||
// for setup-contact, make Alice's one-to-one chat with Bob visible
|
||||
// (secure-join-information are shown in the group chat)
|
||||
@@ -415,10 +413,9 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
.await?
|
||||
.get_addr()
|
||||
.to_owned();
|
||||
if mark_peer_as_verified(context, fingerprint.clone(), contact_addr)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
let fingerprint_found =
|
||||
mark_peer_as_verified(context, fingerprint.clone(), contact_addr).await?;
|
||||
if !fingerprint_found {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_id,
|
||||
@@ -431,7 +428,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited).await?;
|
||||
info!(context, "Auth verified.",);
|
||||
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
|
||||
inviter_progress!(context, contact_id, 600);
|
||||
inviter_progress(context, contact_id, 600);
|
||||
if join_vg {
|
||||
// the vg-member-added message is special:
|
||||
// this is a normal Chat-Group-Member-Added message
|
||||
@@ -446,17 +443,13 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
match chat::get_chat_id_by_grpid(context, field_grpid).await? {
|
||||
Some((group_chat_id, _, _)) => {
|
||||
secure_connection_established(context, contact_id, group_chat_id).await?;
|
||||
if let Err(err) =
|
||||
chat::add_contact_to_chat_ex(context, group_chat_id, contact_id, true)
|
||||
.await
|
||||
{
|
||||
error!(context, "failed to add contact: {}", err);
|
||||
}
|
||||
chat::add_contact_to_chat_ex(context, group_chat_id, contact_id, true)
|
||||
.await?;
|
||||
}
|
||||
None => bail!("Chat {} not found", &field_grpid),
|
||||
}
|
||||
inviter_progress!(context, contact_id, 800);
|
||||
inviter_progress!(context, contact_id, 1000);
|
||||
inviter_progress(context, contact_id, 800);
|
||||
inviter_progress(context, contact_id, 1000);
|
||||
} else {
|
||||
// Alice -> Bob
|
||||
secure_connection_established(
|
||||
@@ -474,53 +467,52 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
.await
|
||||
.context("failed sending vc-contact-confirm message")?;
|
||||
|
||||
inviter_progress!(context, contact_id, 1000);
|
||||
inviter_progress(context, contact_id, 1000);
|
||||
}
|
||||
Ok(HandshakeMessage::Ignore) // "Done" would delete the message and break multi-device (the key from Autocrypt-header is needed)
|
||||
}
|
||||
"vg-member-added" | "vc-contact-confirm" => {
|
||||
/*=======================================================
|
||||
==== Bob - the joiner's side ====
|
||||
==== Step 7 in "Setup verified contact" protocol ====
|
||||
=======================================================*/
|
||||
/*=======================================================
|
||||
==== Bob - the joiner's side ====
|
||||
==== Step 7 in "Setup verified contact" protocol ====
|
||||
=======================================================*/
|
||||
"vc-contact-confirm" => match BobState::from_db(&context.sql).await? {
|
||||
Some(bobstate) => bob::handle_contact_confirm(context, bobstate, mime_message).await,
|
||||
None => Ok(HandshakeMessage::Ignore),
|
||||
},
|
||||
|
||||
"vg-member-added" => {
|
||||
let Some(member_added) = mime_message
|
||||
.get_header(HeaderDef::ChatGroupMemberAdded)
|
||||
.map(|s| s.as_str())
|
||||
else {
|
||||
warn!(
|
||||
context,
|
||||
"vg-member-added without Chat-Group-Member-Added header"
|
||||
);
|
||||
return Ok(HandshakeMessage::Propagate);
|
||||
};
|
||||
if !context.is_self_addr(member_added).await? {
|
||||
info!(
|
||||
context,
|
||||
"Member {member_added} added by unrelated SecureJoin process"
|
||||
);
|
||||
return Ok(HandshakeMessage::Propagate);
|
||||
}
|
||||
match BobState::from_db(&context.sql).await? {
|
||||
Some(bobstate) => {
|
||||
bob::handle_contact_confirm(context, bobstate, mime_message).await
|
||||
}
|
||||
None => match join_vg {
|
||||
true => Ok(HandshakeMessage::Propagate),
|
||||
false => Ok(HandshakeMessage::Ignore),
|
||||
},
|
||||
None => Ok(HandshakeMessage::Propagate),
|
||||
}
|
||||
}
|
||||
|
||||
"vg-member-added-received" | "vc-contact-confirm-received" => {
|
||||
/*==========================================================
|
||||
==== Alice - the inviter side ====
|
||||
==== Step 8 in "Out-of-band verified groups" protocol ====
|
||||
==========================================================*/
|
||||
|
||||
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
|
||||
if contact.is_verified(context).await? == VerifiedStatus::Unverified {
|
||||
warn!(context, "{} invalid.", step);
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
if join_vg {
|
||||
let field_grpid = mime_message
|
||||
.get_header(HeaderDef::SecureJoinGroup)
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or_else(|| "");
|
||||
if let Err(err) = chat::get_chat_id_by_grpid(context, field_grpid).await {
|
||||
warn!(context, "Failed to lookup chat_id from grpid: {}", err);
|
||||
return Err(
|
||||
err.context(format!("Chat for group {} not found", &field_grpid))
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(HandshakeMessage::Ignore) // "Done" deletes the message and breaks multi-device
|
||||
} else {
|
||||
warn!(context, "{} invalid.", step);
|
||||
Ok(HandshakeMessage::Ignore)
|
||||
}
|
||||
Ok(HandshakeMessage::Done) // "Done" deletes the message
|
||||
}
|
||||
_ => {
|
||||
warn!(context, "invalid step: {}", step);
|
||||
@@ -614,21 +606,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
};
|
||||
if let Err(err) = peerstate.set_verified(
|
||||
PeerstateKeyType::GossipKey,
|
||||
fingerprint,
|
||||
PeerstateVerifiedStatus::BidirectVerified,
|
||||
addr,
|
||||
) {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
&format!("Could not mark peer as verified at step {step}: {err}"),
|
||||
)
|
||||
.await?;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
peerstate.set_verified(PeerstateKeyType::GossipKey, fingerprint, addr)?;
|
||||
peerstate.prefer_encrypt = EncryptPreference::Mutual;
|
||||
peerstate.save_to_db(&context.sql).await.unwrap_or_default();
|
||||
} else if let Some(fingerprint) =
|
||||
@@ -637,7 +615,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
// FIXME: Old versions of DC send this header instead of gossips. Remove this
|
||||
// eventually.
|
||||
let fingerprint = fingerprint.parse()?;
|
||||
if mark_peer_as_verified(
|
||||
let fingerprint_found = mark_peer_as_verified(
|
||||
context,
|
||||
fingerprint,
|
||||
Contact::get_by_id(context, contact_id)
|
||||
@@ -645,9 +623,8 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
.get_addr()
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
.await?;
|
||||
if !fingerprint_found {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_id,
|
||||
@@ -672,10 +649,10 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
if step.as_str() == "vg-member-added" {
|
||||
inviter_progress!(context, contact_id, 800);
|
||||
inviter_progress(context, contact_id, 800);
|
||||
}
|
||||
if step.as_str() == "vg-member-added" || step.as_str() == "vc-contact-confirm" {
|
||||
inviter_progress!(context, contact_id, 1000);
|
||||
inviter_progress(context, contact_id, 1000);
|
||||
}
|
||||
if step.as_str() == "vg-request-with-auth" || step.as_str() == "vc-request-with-auth" {
|
||||
// This actually reflects what happens on the first device (which does the secure
|
||||
@@ -705,17 +682,17 @@ async fn secure_connection_established(
|
||||
.get_config_bool(Config::VerifiedOneOnOneChats)
|
||||
.await?
|
||||
{
|
||||
let chat = Chat::load_from_db(context, chat_id).await?;
|
||||
if chat.typ == Chattype::Single {
|
||||
chat_id
|
||||
.set_protection(
|
||||
context,
|
||||
ProtectionStatus::Protected,
|
||||
time(),
|
||||
Some(contact_id),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
let private_chat_id = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Yes)
|
||||
.await?
|
||||
.id;
|
||||
private_chat_id
|
||||
.set_protection(
|
||||
context,
|
||||
ProtectionStatus::Protected,
|
||||
time(),
|
||||
Some(contact_id),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
Ok(())
|
||||
@@ -737,27 +714,21 @@ async fn could_not_establish_secure_connection(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tries to mark peer with provided key fingerprint as verified.
|
||||
///
|
||||
/// Returns true if such key was found, false otherwise.
|
||||
async fn mark_peer_as_verified(
|
||||
context: &Context,
|
||||
fingerprint: Fingerprint,
|
||||
verifier: String,
|
||||
) -> Result<()> {
|
||||
if let Some(ref mut peerstate) = Peerstate::from_fingerprint(context, &fingerprint).await? {
|
||||
if let Err(err) = peerstate.set_verified(
|
||||
PeerstateKeyType::PublicKey,
|
||||
fingerprint,
|
||||
PeerstateVerifiedStatus::BidirectVerified,
|
||||
verifier,
|
||||
) {
|
||||
error!(context, "Could not mark peer as verified: {}", err);
|
||||
return Err(err);
|
||||
}
|
||||
peerstate.prefer_encrypt = EncryptPreference::Mutual;
|
||||
peerstate.save_to_db(&context.sql).await.unwrap_or_default();
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("no peerstate in db for fingerprint {}", fingerprint.hex());
|
||||
}
|
||||
) -> Result<bool> {
|
||||
let Some(ref mut peerstate) = Peerstate::from_fingerprint(context, &fingerprint).await? else {
|
||||
return Ok(false);
|
||||
};
|
||||
peerstate.set_verified(PeerstateKeyType::PublicKey, fingerprint, verifier)?;
|
||||
peerstate.prefer_encrypt = EncryptPreference::Mutual;
|
||||
peerstate.save_to_db(&context.sql).await.unwrap_or_default();
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/* ******************************************************************************
|
||||
@@ -794,6 +765,7 @@ mod tests {
|
||||
use crate::chat;
|
||||
use crate::chat::ProtectionStatus;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::contact::ContactAddress;
|
||||
use crate::contact::VerifiedStatus;
|
||||
use crate::peerstate::Peerstate;
|
||||
|
||||
@@ -15,6 +15,7 @@ use crate::contact::Contact;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::mimeparser::MimeMessage;
|
||||
use crate::sync::Sync::*;
|
||||
use crate::tools::time;
|
||||
use crate::{chat, stock_str};
|
||||
|
||||
@@ -110,7 +111,7 @@ pub(super) async fn handle_auth_required(
|
||||
/// Handles `vc-contact-confirm` and `vg-member-added` handshake messages.
|
||||
///
|
||||
/// # Bob - the joiner's side
|
||||
/// ## Step 4 in the "Setup Contact protocol"
|
||||
/// ## Step 7 in the "Setup Contact protocol"
|
||||
pub(super) async fn handle_contact_confirm(
|
||||
context: &Context,
|
||||
mut bobstate: BobState,
|
||||
@@ -131,6 +132,7 @@ pub(super) async fn handle_contact_confirm(
|
||||
// verify both contacts (this could be a bug/security issue, see
|
||||
// e.g. https://github.com/deltachat/deltachat-core-rust/issues/1177).
|
||||
bobstate.notify_peer_verified(context).await?;
|
||||
bobstate.emit_progress(context, JoinerProgress::Succeeded);
|
||||
Ok(retval)
|
||||
}
|
||||
Some(_) => {
|
||||
@@ -179,7 +181,7 @@ impl BobState {
|
||||
} => {
|
||||
let group_chat_id = match chat::get_chat_id_by_grpid(context, grpid).await? {
|
||||
Some((chat_id, _protected, _blocked)) => {
|
||||
chat_id.unblock(context).await?;
|
||||
chat_id.unblock_ex(context, Nosync).await?;
|
||||
chat_id
|
||||
}
|
||||
None => {
|
||||
@@ -227,9 +229,8 @@ impl BobState {
|
||||
if context
|
||||
.get_config_bool(Config::VerifiedOneOnOneChats)
|
||||
.await?
|
||||
&& chat_id == self.alice_chat()
|
||||
{
|
||||
chat_id
|
||||
self.alice_chat()
|
||||
.set_protection(
|
||||
context,
|
||||
ProtectionStatus::Protected,
|
||||
@@ -255,8 +256,8 @@ enum JoinerProgress {
|
||||
///
|
||||
/// Typically shows as "alice@addr verified, introducing myself."
|
||||
RequestWithAuthSent,
|
||||
// /// Completed securejoin.
|
||||
// Succeeded,
|
||||
/// Completed securejoin.
|
||||
Succeeded,
|
||||
}
|
||||
|
||||
impl From<JoinerProgress> for usize {
|
||||
@@ -264,7 +265,7 @@ impl From<JoinerProgress> for usize {
|
||||
match progress {
|
||||
JoinerProgress::Error => 0,
|
||||
JoinerProgress::RequestWithAuthSent => 400,
|
||||
// JoinerProgress::Succeeded => 1000,
|
||||
JoinerProgress::Succeeded => 1000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//! The [`BobState`] is only directly used to initially create it when starting the
|
||||
//! protocol.
|
||||
|
||||
use anyhow::{Error, Result};
|
||||
use anyhow::Result;
|
||||
use rusqlite::Connection;
|
||||
|
||||
use super::qrinvite::QrInvite;
|
||||
@@ -335,36 +335,6 @@ impl BobState {
|
||||
context,
|
||||
"Bob Step 7 - handling vc-contact-confirm/vg-member-added message"
|
||||
);
|
||||
let vg_expect_encrypted = match self.invite {
|
||||
QrInvite::Contact { .. } => {
|
||||
// setup-contact is always encrypted
|
||||
true
|
||||
}
|
||||
QrInvite::Group { ref grpid, .. } => {
|
||||
// This is buggy, result will always be
|
||||
// false since the group is created by receive_imf for
|
||||
// the very handshake message we're handling now. But
|
||||
// only after we have returned. It does not impact
|
||||
// the security invariants of secure-join however.
|
||||
|
||||
chat::get_chat_id_by_grpid(context, grpid)
|
||||
.await?
|
||||
.map_or(false, |(_chat_id, is_protected, _blocked)| is_protected)
|
||||
// when joining a non-verified group
|
||||
// the vg-member-added message may be unencrypted
|
||||
// when not all group members have keys or prefer encryption.
|
||||
// So only expect encryption if this is a verified group
|
||||
}
|
||||
};
|
||||
if vg_expect_encrypted
|
||||
&& !encrypted_and_signed(context, mime_message, Some(self.invite.fingerprint()))
|
||||
{
|
||||
self.update_next(&context.sql, SecureJoinStep::Terminated)
|
||||
.await?;
|
||||
return Ok(Some(BobHandshakeStage::Terminated(
|
||||
"Contact confirm message not encrypted",
|
||||
)));
|
||||
}
|
||||
mark_peer_as_verified(
|
||||
context,
|
||||
self.invite.fingerprint().clone(),
|
||||
@@ -375,17 +345,6 @@ impl BobState {
|
||||
.await?;
|
||||
context.emit_event(EventType::ContactsChanged(None));
|
||||
|
||||
if let QrInvite::Group { .. } = self.invite {
|
||||
let member_added = mime_message
|
||||
.get_header(HeaderDef::ChatGroupMemberAdded)
|
||||
.map(|s| s.as_str())
|
||||
.ok_or_else(|| Error::msg("Missing Chat-Group-Member-Added header"))?;
|
||||
if !context.is_self_addr(member_added).await? {
|
||||
info!(context, "Message belongs to a different handshake (scaled up contact anyway to allow creation of group).");
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
self.send_handshake_message(context, BobHandshakeMsg::ContactConfirmReceived)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
|
||||
+17
-1
@@ -413,6 +413,12 @@ pub enum StockMessage {
|
||||
|
||||
#[strum(props(fallback = "%1$s sent a message from another device."))]
|
||||
ChatProtectionDisabled = 171,
|
||||
|
||||
#[strum(props(fallback = "Others will only see this group after you sent a first message."))]
|
||||
NewGroupSendFirstMessage = 172,
|
||||
|
||||
#[strum(props(fallback = "Member %1$s added."))]
|
||||
MsgAddMember = 173,
|
||||
}
|
||||
|
||||
impl StockMessage {
|
||||
@@ -600,6 +606,7 @@ pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId
|
||||
}
|
||||
|
||||
/// Stock string: `I added member %1$s.`.
|
||||
/// This one is for sending in group chats.
|
||||
///
|
||||
/// The `added_member_addr` parameter should be an email address and is looked up in the
|
||||
/// contacts to combine with the authorized display name.
|
||||
@@ -634,7 +641,11 @@ pub(crate) async fn msg_add_member_local(
|
||||
.unwrap_or_else(|_| addr.to_string()),
|
||||
_ => addr.to_string(),
|
||||
};
|
||||
if by_contact == ContactId::SELF {
|
||||
if by_contact == ContactId::UNDEFINED {
|
||||
translated(context, StockMessage::MsgAddMember)
|
||||
.await
|
||||
.replace1(whom)
|
||||
} else if by_contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouAddMember)
|
||||
.await
|
||||
.replace1(whom)
|
||||
@@ -1284,6 +1295,11 @@ pub(crate) async fn aeap_explanation_and_link(
|
||||
.replace2(new_addr)
|
||||
}
|
||||
|
||||
/// Stock string: `Others will only see this group after you sent a first message.`.
|
||||
pub(crate) async fn new_group_send_first_message(context: &Context) -> String {
|
||||
translated(context, StockMessage::NewGroupSendFirstMessage).await
|
||||
}
|
||||
|
||||
/// Text to put in the [`Qr::Backup`] rendered SVG image.
|
||||
///
|
||||
/// The default is "Scan to set up second device for <account name (account addr)>". The
|
||||
|
||||
+213
-9
@@ -5,7 +5,7 @@ use lettre_email::mime::{self};
|
||||
use lettre_email::PartBuilder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::chat::{Chat, ChatId};
|
||||
use crate::chat::{self, Chat, ChatVisibility};
|
||||
use crate::config::Config;
|
||||
use crate::constants::Blocked;
|
||||
use crate::contact::ContactId;
|
||||
@@ -13,10 +13,26 @@ use crate::context::Context;
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Param;
|
||||
use crate::sync::SyncData::{AddQrToken, DeleteQrToken};
|
||||
use crate::sync::SyncData::{AddQrToken, AlterChat, DeleteQrToken};
|
||||
use crate::token::Namespace;
|
||||
use crate::tools::time;
|
||||
use crate::{chat, stock_str, token};
|
||||
use crate::{stock_str, token};
|
||||
|
||||
/// Whether to send device sync messages. Aimed for usage in the internal API.
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Sync {
|
||||
Nosync,
|
||||
Sync,
|
||||
}
|
||||
|
||||
impl From<Sync> for bool {
|
||||
fn from(sync: Sync) -> bool {
|
||||
match sync {
|
||||
Sync::Nosync => false,
|
||||
Sync::Sync => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct QrTokenData {
|
||||
@@ -25,10 +41,28 @@ pub(crate) struct QrTokenData {
|
||||
pub(crate) grpid: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub(crate) enum ChatId {
|
||||
ContactAddr(String),
|
||||
Grpid(String),
|
||||
// NOTE: Ad-hoc groups lack an identifier that can be used across devices so
|
||||
// block/mute/etc. actions on them are not synchronized to other devices.
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub(crate) enum ChatAction {
|
||||
Block,
|
||||
Unblock,
|
||||
Accept,
|
||||
SetVisibility(ChatVisibility),
|
||||
SetMuted(chat::MuteDuration),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(crate) enum SyncData {
|
||||
AddQrToken(QrTokenData),
|
||||
DeleteQrToken(QrTokenData),
|
||||
AlterChat { id: ChatId, action: ChatAction },
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -44,6 +78,9 @@ pub(crate) struct SyncItems {
|
||||
|
||||
impl Context {
|
||||
/// Adds an item to the list of items that should be synchronized to other devices.
|
||||
///
|
||||
/// NB: Private and `pub(crate)` functions shouldn't call this unless `Sync::Sync` is explicitly
|
||||
/// passed to them. This way it's always clear whether the code performs synchronisation.
|
||||
pub(crate) async fn add_sync_item(&self, data: SyncData) -> Result<()> {
|
||||
self.add_sync_item_with_timestamp(data, time()).await
|
||||
}
|
||||
@@ -67,7 +104,7 @@ impl Context {
|
||||
/// Adds most recent qr-code tokens for a given chat to the list of items to be synced.
|
||||
/// If device synchronization is disabled,
|
||||
/// no tokens exist or the chat is unpromoted, the function does nothing.
|
||||
pub(crate) async fn sync_qr_code_tokens(&self, chat_id: Option<ChatId>) -> Result<()> {
|
||||
pub(crate) async fn sync_qr_code_tokens(&self, chat_id: Option<chat::ChatId>) -> Result<()> {
|
||||
if !self.get_config_bool(Config::SyncMsgs).await? {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -118,7 +155,7 @@ impl Context {
|
||||
pub async fn send_sync_msg(&self) -> Result<Option<MsgId>> {
|
||||
if let Some((json, ids)) = self.build_sync_json().await? {
|
||||
let chat_id =
|
||||
ChatId::create_for_contact_with_blocked(self, ContactId::SELF, Blocked::Yes)
|
||||
chat::ChatId::create_for_contact_with_blocked(self, ContactId::SELF, Blocked::Yes)
|
||||
.await?;
|
||||
let mut msg = Message {
|
||||
chat_id,
|
||||
@@ -204,7 +241,7 @@ impl Context {
|
||||
Ok(sync_items)
|
||||
}
|
||||
|
||||
/// Execute sync items.
|
||||
/// Executes sync items sent by other device.
|
||||
///
|
||||
/// CAVE: When changing the code to handle other sync items,
|
||||
/// take care that does not result in calls to `add_sync_item()`
|
||||
@@ -243,6 +280,7 @@ impl Context {
|
||||
token::delete(self, Namespace::InviteNumber, &token.invitenumber).await?;
|
||||
token::delete(self, Namespace::Auth, &token.auth).await?;
|
||||
}
|
||||
AlterChat { id, action } => self.sync_alter_chat(id, action).await?,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -251,11 +289,15 @@ impl Context {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::bail;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::Chat;
|
||||
use crate::chat::{Chat, ProtectionStatus};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::contact::{Contact, Origin};
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::token::Namespace;
|
||||
|
||||
@@ -277,6 +319,20 @@ mod tests {
|
||||
|
||||
assert!(t.build_sync_json().await?.is_none());
|
||||
|
||||
// Having one test on `SyncData::AlterChat` is sufficient here as `ChatAction::SetMuted`
|
||||
// introduces enums inside items and `SystemTime`. Let's avoid in-depth testing of the
|
||||
// serialiser here which is an external crate.
|
||||
t.add_sync_item_with_timestamp(
|
||||
SyncData::AlterChat {
|
||||
id: ChatId::ContactAddr("bob@example.net".to_string()),
|
||||
action: ChatAction::SetMuted(chat::MuteDuration::Until(
|
||||
SystemTime::UNIX_EPOCH + Duration::from_millis(42999),
|
||||
)),
|
||||
},
|
||||
1631781315,
|
||||
)
|
||||
.await?;
|
||||
|
||||
t.add_sync_item_with_timestamp(
|
||||
SyncData::AddQrToken(QrTokenData {
|
||||
invitenumber: "testinvite".to_string(),
|
||||
@@ -300,6 +356,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
serialized,
|
||||
r#"{"items":[
|
||||
{"timestamp":1631781315,"data":{"AlterChat":{"id":{"ContactAddr":"bob@example.net"},"action":{"SetMuted":{"Until":{"secs_since_epoch":42,"nanos_since_epoch":999000000}}}}}},
|
||||
{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"testinvite","auth":"testauth","grpid":"group123"}}},
|
||||
{"timestamp":1631781317,"data":{"DeleteQrToken":{"invitenumber":"123!?\":.;{}","auth":"456","grpid":null}}}
|
||||
]}"#
|
||||
@@ -310,7 +367,7 @@ mod tests {
|
||||
assert!(t.build_sync_json().await?.is_none());
|
||||
|
||||
let sync_items = t.parse_sync_items(serialized)?;
|
||||
assert_eq!(sync_items.items.len(), 2);
|
||||
assert_eq!(sync_items.items.len(), 3);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -368,6 +425,27 @@ mod tests {
|
||||
)
|
||||
.is_err()); // missing field
|
||||
|
||||
assert!(t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781318,"data":{"AlterChat":{"id":{"ContactAddr":"bob@example.net"},"action":"Burn"}}}]}"#.to_string(),
|
||||
)
|
||||
.is_err()); // Unknown enum value
|
||||
|
||||
// Test enums inside items and SystemTime
|
||||
let sync_items = t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781318,"data":{"AlterChat":{"id":{"ContactAddr":"bob@example.net"},"action":{"SetMuted":{"Until":{"secs_since_epoch":42,"nanos_since_epoch":999000000}}}}}}]}"#.to_string(),
|
||||
)?;
|
||||
assert_eq!(sync_items.items.len(), 1);
|
||||
let AlterChat { id, action } = &sync_items.items.get(0).unwrap().data else {
|
||||
bail!("bad item");
|
||||
};
|
||||
assert_eq!(*id, ChatId::ContactAddr("bob@example.net".to_string()));
|
||||
assert_eq!(
|
||||
*action,
|
||||
ChatAction::SetMuted(chat::MuteDuration::Until(
|
||||
SystemTime::UNIX_EPOCH + Duration::from_millis(42999)
|
||||
))
|
||||
);
|
||||
|
||||
// empty item list is okay
|
||||
assert_eq!(
|
||||
t.parse_sync_items(r#"{"items":[]}"#.to_string())?
|
||||
@@ -423,6 +501,7 @@ mod tests {
|
||||
let sync_items = t
|
||||
.parse_sync_items(
|
||||
r#"{"items":[
|
||||
{"timestamp":1631781315,"data":{"AlterChat":{"id":{"ContactAddr":"bob@example.net"},"action":"Block"}}},
|
||||
{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"yip-in","auth":"a"}}},
|
||||
{"timestamp":1631781316,"data":{"DeleteQrToken":{"invitenumber":"in","auth":"delete unexistent, shall continue"}}},
|
||||
{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":"yip-auth"}}},
|
||||
@@ -435,6 +514,11 @@ mod tests {
|
||||
?;
|
||||
t.execute_sync_items(&sync_items).await?;
|
||||
|
||||
assert!(
|
||||
Contact::lookup_id_by_addr(&t, "bob@example.net", Origin::Unknown)
|
||||
.await?
|
||||
.is_none()
|
||||
);
|
||||
assert!(token::exists(&t, Namespace::InviteNumber, "yip-in").await);
|
||||
assert!(token::exists(&t, Namespace::Auth, "yip-auth").await);
|
||||
assert!(!token::exists(&t, Namespace::Auth, "non-existent").await);
|
||||
@@ -462,7 +546,7 @@ mod tests {
|
||||
// check that the used self-talk is not visible to the user
|
||||
// but that creation will still work (in this case, the chat is empty)
|
||||
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
|
||||
let chat_id = ChatId::create_for_contact(&alice, ContactId::SELF).await?;
|
||||
let chat_id = chat::ChatId::create_for_contact(&alice, ContactId::SELF).await?;
|
||||
let chat = Chat::load_from_db(&alice, chat_id).await?;
|
||||
assert!(chat.is_self_talk());
|
||||
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
|
||||
@@ -485,4 +569,124 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_alter_chat() -> Result<()> {
|
||||
let alices = [
|
||||
TestContext::new_alice().await,
|
||||
TestContext::new_alice().await,
|
||||
];
|
||||
for a in &alices {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let ba_chat = bob.create_chat(&alices[0]).await;
|
||||
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
|
||||
let a0b_chat_id = alices[0].recv_msg(&sent_msg).await.chat_id;
|
||||
alices[1].recv_msg(&sent_msg).await;
|
||||
|
||||
async fn sync(alices: &[TestContext]) -> Result<()> {
|
||||
let sync_msg = alices.get(0).unwrap().pop_sent_msg().await;
|
||||
alices.get(1).unwrap().recv_msg(&sync_msg).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Request);
|
||||
a0b_chat_id.accept(&alices[0]).await?;
|
||||
sync(&alices).await?;
|
||||
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Not);
|
||||
a0b_chat_id.block(&alices[0]).await?;
|
||||
sync(&alices).await?;
|
||||
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Yes);
|
||||
a0b_chat_id.unblock(&alices[0]).await?;
|
||||
sync(&alices).await?;
|
||||
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Not);
|
||||
|
||||
// Unblocking a 1:1 chat doesn't unblock the contact currently.
|
||||
let a0b_contact_id = alices[0].add_or_lookup_contact(&bob).await.id;
|
||||
Contact::unblock(&alices[0], a0b_contact_id).await?;
|
||||
|
||||
assert!(!alices[1].add_or_lookup_contact(&bob).await.is_blocked());
|
||||
Contact::block(&alices[0], a0b_contact_id).await?;
|
||||
sync(&alices).await?;
|
||||
assert!(alices[1].add_or_lookup_contact(&bob).await.is_blocked());
|
||||
Contact::unblock(&alices[0], a0b_contact_id).await?;
|
||||
sync(&alices).await?;
|
||||
assert!(!alices[1].add_or_lookup_contact(&bob).await.is_blocked());
|
||||
|
||||
// Test accepting and blocking groups. This way we test:
|
||||
// - Group chats synchronisation.
|
||||
// - That blocking a group deletes it on other devices.
|
||||
let fiona = TestContext::new_fiona().await;
|
||||
let fiona_grp_chat_id = fiona
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&alices[0]])
|
||||
.await;
|
||||
let sent_msg = fiona.send_text(fiona_grp_chat_id, "hi").await;
|
||||
let a0_grp_chat_id = alices[0].recv_msg(&sent_msg).await.chat_id;
|
||||
let a1_grp_chat_id = alices[1].recv_msg(&sent_msg).await.chat_id;
|
||||
let a1_grp_chat = Chat::load_from_db(&alices[1], a1_grp_chat_id).await?;
|
||||
assert_eq!(a1_grp_chat.blocked, Blocked::Request);
|
||||
a0_grp_chat_id.accept(&alices[0]).await?;
|
||||
sync(&alices).await?;
|
||||
let a1_grp_chat = Chat::load_from_db(&alices[1], a1_grp_chat_id).await?;
|
||||
assert_eq!(a1_grp_chat.blocked, Blocked::Not);
|
||||
a0_grp_chat_id.block(&alices[0]).await?;
|
||||
sync(&alices).await?;
|
||||
assert!(Chat::load_from_db(&alices[1], a1_grp_chat_id)
|
||||
.await
|
||||
.is_err());
|
||||
assert!(
|
||||
!alices[1]
|
||||
.sql
|
||||
.exists("SELECT COUNT(*) FROM chats WHERE id=?", (a1_grp_chat_id,))
|
||||
.await?
|
||||
);
|
||||
|
||||
// Test syncing of chat visibility on a self-chat. This way we test:
|
||||
// - Self-chat synchronisation.
|
||||
// - That sync messages don't unarchive the self-chat.
|
||||
let a0self_chat_id = alices[0].get_self_chat().await.id;
|
||||
assert_eq!(
|
||||
alices[1].get_self_chat().await.get_visibility(),
|
||||
ChatVisibility::Normal
|
||||
);
|
||||
let mut visibilities =
|
||||
ChatVisibility::iter().chain(std::iter::once(ChatVisibility::Normal));
|
||||
visibilities.next();
|
||||
for v in visibilities {
|
||||
a0self_chat_id.set_visibility(&alices[0], v).await?;
|
||||
sync(&alices).await?;
|
||||
for a in &alices {
|
||||
assert_eq!(a.get_self_chat().await.get_visibility(), v);
|
||||
}
|
||||
}
|
||||
|
||||
use chat::MuteDuration;
|
||||
assert_eq!(
|
||||
alices[1].get_chat(&bob).await.mute_duration,
|
||||
MuteDuration::NotMuted
|
||||
);
|
||||
let mute_durations = [
|
||||
MuteDuration::Forever,
|
||||
MuteDuration::Until(SystemTime::now() + Duration::from_secs(42)),
|
||||
MuteDuration::NotMuted,
|
||||
];
|
||||
for m in mute_durations {
|
||||
chat::set_muted(&alices[0], a0b_chat_id, m).await?;
|
||||
sync(&alices).await?;
|
||||
let m = match m {
|
||||
MuteDuration::Until(time) => MuteDuration::Until(
|
||||
SystemTime::UNIX_EPOCH
|
||||
+ Duration::from_secs(
|
||||
time.duration_since(SystemTime::UNIX_EPOCH)?.as_secs(),
|
||||
),
|
||||
),
|
||||
_ => m,
|
||||
};
|
||||
assert_eq!(alices[1].get_chat(&bob).await.mute_duration, m);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -374,6 +374,11 @@ impl TestContext {
|
||||
}
|
||||
});
|
||||
|
||||
ctx.set_config(Config::SkipStartMessages, Some("1"))
|
||||
.await
|
||||
.unwrap();
|
||||
ctx.set_config(Config::SyncMsgs, Some("0")).await.unwrap();
|
||||
|
||||
Self {
|
||||
ctx,
|
||||
dir,
|
||||
@@ -509,12 +514,7 @@ impl TestContext {
|
||||
.await
|
||||
.expect("receive_imf() seems not to have added a new message to the db");
|
||||
|
||||
assert_eq!(
|
||||
received.msg_ids.len(),
|
||||
1,
|
||||
"recv_msg() can currently only receive messages with exactly one part"
|
||||
);
|
||||
let msg = Message::load_from_db(self, received.msg_ids[0])
|
||||
let msg = Message::load_from_db(self, *received.msg_ids.last().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
Group#Chat#10: Group chat [4 member(s)]
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#10: (Contact#Contact#10): Hi! I created a group. [FRESH]
|
||||
Msg#11: Me (Contact#Contact#Self): You left the group. [INFO] o
|
||||
Msg#12: (Contact#Contact#10): Member claire@example.net added by alice@example.org. [FRESH][INFO]
|
||||
Msg#13: (Contact#Contact#10): Member Me (bob@example.net) added. [FRESH][INFO]
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -0,0 +1,80 @@
|
||||
From - Thu, 02 Nov 2023 05:25:44 GMT
|
||||
X-Mozilla-Status: 0801
|
||||
X-Mozilla-Status2: 00000000
|
||||
Message-ID: <619b4cc5-1b76-2bfc-02ee-631b774bcb4a@example.org>
|
||||
Date: Thu, 2 Nov 2023 02:25:44 -0300
|
||||
MIME-Version: 1.0
|
||||
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101
|
||||
Thunderbird/102.13.0
|
||||
Content-Language: en-US
|
||||
To: bob@example.net
|
||||
From: Alice <alice@example.org>
|
||||
X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0;
|
||||
attachmentreminder=0; deliveryformat=0
|
||||
X-Identity-Key: id3
|
||||
Fcc: imap://alice%40example.org@in.example.org/Sent
|
||||
Subject: ...
|
||||
Content-Type: multipart/encrypted;
|
||||
protocol="application/pgp-encrypted";
|
||||
boundary="------------ODMSY6iRxlSk4uG4zIxjzLbw"
|
||||
|
||||
This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)
|
||||
--------------ODMSY6iRxlSk4uG4zIxjzLbw
|
||||
Content-Type: application/pgp-encrypted
|
||||
Content-Description: PGP/MIME version identification
|
||||
|
||||
Version: 1
|
||||
|
||||
--------------ODMSY6iRxlSk4uG4zIxjzLbw
|
||||
Content-Type: application/octet-stream; name="encrypted.asc"
|
||||
Content-Description: OpenPGP encrypted message
|
||||
Content-Disposition: inline; filename="encrypted.asc"
|
||||
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
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
|
||||
d+EkgKXHfQVfohZhSg==
|
||||
=u1yY
|
||||
-----END PGP MESSAGE-----
|
||||
|
||||
--------------ODMSY6iRxlSk4uG4zIxjzLbw--
|
||||
@@ -0,0 +1,165 @@
|
||||
From - Thu, 02 Nov 2023 05:20:27 GMT
|
||||
X-Mozilla-Status: 0801
|
||||
X-Mozilla-Status2: 00000000
|
||||
Message-ID: <956fad6d-206e-67af-2443-3ea5819418ff@example.org>
|
||||
Date: Thu, 2 Nov 2023 02:20:27 -0300
|
||||
MIME-Version: 1.0
|
||||
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101
|
||||
Thunderbird/102.13.0
|
||||
Content-Language: en-US
|
||||
To: bob@example.net
|
||||
From: Alice <alice@example.org>
|
||||
Autocrypt: addr=alice@example.org; keydata=
|
||||
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
|
||||
/qMLjKwBpKEd/w==
|
||||
X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0;
|
||||
attachmentreminder=0; deliveryformat=0
|
||||
X-Identity-Key: id3
|
||||
Fcc: imap://alice%40example.org@in.example.org/Sent
|
||||
Subject: ...
|
||||
Content-Type: multipart/encrypted;
|
||||
protocol="application/pgp-encrypted";
|
||||
boundary="------------vm4cgC4UBxQhCLFg6Vv0nrTd"
|
||||
|
||||
This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)
|
||||
--------------vm4cgC4UBxQhCLFg6Vv0nrTd
|
||||
Content-Type: application/pgp-encrypted
|
||||
Content-Description: PGP/MIME version identification
|
||||
|
||||
Version: 1
|
||||
|
||||
--------------vm4cgC4UBxQhCLFg6Vv0nrTd
|
||||
Content-Type: application/octet-stream; name="encrypted.asc"
|
||||
Content-Description: OpenPGP encrypted message
|
||||
Content-Disposition: inline; filename="encrypted.asc"
|
||||
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
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
|
||||
=UKyr
|
||||
-----END PGP MESSAGE-----
|
||||
|
||||
--------------vm4cgC4UBxQhCLFg6Vv0nrTd--
|
||||
Reference in New Issue
Block a user