Compare commits

..

1 Commits

Author SHA1 Message Date
iequidoo
eae8b13c8b doc: create_group_chat_unencrypted() may lead to chat split on the first device 2025-07-25 07:15:37 -03:00
76 changed files with 649 additions and 1774 deletions

View File

@@ -20,7 +20,7 @@ permissions: {}
env:
RUSTFLAGS: -Dwarnings
RUST_VERSION: 1.89.0
RUST_VERSION: 1.88.0
# Minimum Supported Rust Version
MSRV: 1.85.0
@@ -30,7 +30,7 @@ jobs:
name: Lint Rust
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false
@@ -53,7 +53,7 @@ jobs:
name: cargo deny
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false
@@ -67,7 +67,7 @@ jobs:
name: Check provider database
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false
@@ -80,7 +80,7 @@ jobs:
env:
RUSTDOCFLAGS: -Dwarnings
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false
@@ -115,7 +115,7 @@ jobs:
shell: bash
if: matrix.rust == 'latest'
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false
@@ -154,7 +154,7 @@ jobs:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false
@@ -179,7 +179,7 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false
@@ -201,7 +201,7 @@ jobs:
name: Python lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false
@@ -244,13 +244,13 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false
- name: Download libdeltachat.a
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
name: ${{ matrix.os }}-libdeltachat.a
path: target/debug
@@ -297,7 +297,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false
@@ -311,7 +311,7 @@ jobs:
run: pip install tox
- name: Download deltachat-rpc-server
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
name: ${{ matrix.os }}-deltachat-rpc-server
path: target/debug

View File

@@ -30,7 +30,7 @@ jobs:
arch: [aarch64, armv7l, armv6l, i686, x86_64]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false
@@ -54,7 +54,7 @@ jobs:
arch: [win32, win64]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false
@@ -79,7 +79,7 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false
@@ -105,7 +105,7 @@ jobs:
arch: [arm64-v8a, armeabi-v7a]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false
@@ -132,74 +132,74 @@ jobs:
contents: write
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false
- uses: DeterminateSystems/nix-installer-action@main
- name: Download Linux aarch64 binary
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-aarch64-linux
path: deltachat-rpc-server-aarch64-linux.d
- name: Download Linux armv7l binary
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-armv7l-linux
path: deltachat-rpc-server-armv7l-linux.d
- name: Download Linux armv6l binary
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-armv6l-linux
path: deltachat-rpc-server-armv6l-linux.d
- name: Download Linux i686 binary
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-i686-linux
path: deltachat-rpc-server-i686-linux.d
- name: Download Linux x86_64 binary
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-x86_64-linux
path: deltachat-rpc-server-x86_64-linux.d
- name: Download Win32 binary
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-win32
path: deltachat-rpc-server-win32.d
- name: Download Win64 binary
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-win64
path: deltachat-rpc-server-win64.d
- name: Download macOS binary for x86_64
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-x86_64-macos
path: deltachat-rpc-server-x86_64-macos.d
- name: Download macOS binary for aarch64
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-aarch64-macos
path: deltachat-rpc-server-aarch64-macos.d
- name: Download Android binary for arm64-v8a
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-arm64-v8a-android
path: deltachat-rpc-server-arm64-v8a-android.d
- name: Download Android binary for armeabi-v7a
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-armeabi-v7a-android
path: deltachat-rpc-server-armeabi-v7a-android.d
@@ -285,7 +285,7 @@ jobs:
# Needed to publish the binaries to the release.
contents: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false
@@ -294,67 +294,67 @@ jobs:
python-version: "3.11"
- name: Download Linux aarch64 binary
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-aarch64-linux
path: deltachat-rpc-server-aarch64-linux.d
- name: Download Linux armv7l binary
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-armv7l-linux
path: deltachat-rpc-server-armv7l-linux.d
- name: Download Linux armv6l binary
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-armv6l-linux
path: deltachat-rpc-server-armv6l-linux.d
- name: Download Linux i686 binary
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-i686-linux
path: deltachat-rpc-server-i686-linux.d
- name: Download Linux x86_64 binary
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-x86_64-linux
path: deltachat-rpc-server-x86_64-linux.d
- name: Download Win32 binary
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-win32
path: deltachat-rpc-server-win32.d
- name: Download Win64 binary
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-win64
path: deltachat-rpc-server-win64.d
- name: Download macOS binary for x86_64
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-x86_64-macos
path: deltachat-rpc-server-x86_64-macos.d
- name: Download macOS binary for aarch64
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-aarch64-macos
path: deltachat-rpc-server-aarch64-macos.d
- name: Download Android binary for arm64-v8a
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-arm64-v8a-android
path: deltachat-rpc-server-arm64-v8a-android.d
- name: Download Android binary for armeabi-v7a
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-armeabi-v7a-android
path: deltachat-rpc-server-armeabi-v7a-android.d

View File

@@ -14,7 +14,7 @@ jobs:
id-token: write
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false

View File

@@ -16,7 +16,7 @@ jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false

View File

@@ -19,7 +19,7 @@ jobs:
name: check flake formatting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false
@@ -81,7 +81,7 @@ jobs:
#- deltachat-rpc-server-x86_64-android
#- deltachat-rpc-server-x86-android
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false
@@ -101,7 +101,7 @@ jobs:
# - deltachat-rpc-server-aarch64-darwin
# - deltachat-rpc-server-x86_64-darwin
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false
@@ -42,7 +42,7 @@ jobs:
steps:
- name: Download all the dists
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/

View File

@@ -14,7 +14,7 @@ jobs:
name: Build REPL example
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false
@@ -31,7 +31,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false
@@ -50,7 +50,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false
@@ -72,7 +72,7 @@ jobs:
working-directory: ./deltachat-jsonrpc/typescript
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false

View File

@@ -14,7 +14,7 @@ jobs:
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
persist-credentials: false

View File

@@ -1,145 +1,5 @@
# Changelog
## [2.12.0] - 2025-08-26
### API-Changes
- api!(python): remove remaining broken API for reactions
### Features / Changes
- Use Group ID for chat color generation instead of the name for encrypted groups.
- Use key fingerprints instead of addresses for key-contacts color generation.
- Replace HSLuv colors with OKLCh.
- `wal_checkpoint()`: Do `wal_checkpoint(PASSIVE)` and `wal_checkpoint(FULL)` before `wal_checkpoint(TRUNCATE)`.
- Assign messages to key-contacts based on Issuer Fingerprint.
- Create_group_ex(): Log and replace invalid chat name with "…".
### Fixes
- Do not create a group if the sender includes self in the `To` field.
- Do not reverify already verified contacts via gossip.
- `get_connectivity()`: Get rid of locking SchedulerState::inner ([#7124](https://github.com/chatmail/core/pull/7124)).
- Make reaction message hidden only if there are no other parts.
### Refactor
- Do not return `Result` from `valid_signature_fingerprints()`.
- Make `ConnectivityStore` use a non-async lock ([#7129](https://github.com/chatmail/core/pull/7129)).
### Documentation
- Remove broken link from documentation comments.
- Remove the comment about Color Vision Deficiency correction.
## [2.11.0] - 2025-08-13
### Features / Changes
- Contact::lookup_id_by_addr_ex: Prefer returning key-contact.
- Contact::lookup_id_by_addr_ex: Prefer returning accepted contacts.
- Better string when using disappearing messages of one year (365..367 days, so it can be tweaked later).
- Do not require resent messages to be from the same chat.
- `lookup_key_contact_by_address()`: Allow looking up ContactId::SELF without chat id.
- `get_securejoin_qr()`: Log error if group doesn't have grpid.
- `receive_imf::add_parts()`: Get rid of extra `Chat::load_from_db()` calls.
### Fixes
- Ignore case when trying to detect 'invalid unencrypted mail' and add an info-message.
- Run wal_checkpoint during housekeeping ([#6089](https://github.com/chatmail/core/pull/6089)).
- Allow receiving empty files.
- Set correct sent_timestamp for saved outgoing messages.
- Do not remove query parameters from URLs.
- Log and set imex progress error ([#7091](https://github.com/chatmail/core/pull/7091)).
- Do not add key-contacts to unencrypted groups.
- Do not reset `GuaranteeE2ee` in the database when resending messages.
- Assign messages to a group if there is a `Chat-Group-Name`.
- Take `Chat-Group-Name` into account when matching ad hoc groups.
- Don't break long group names with non-ASCII characters.
- Add messages that can't be verified as `DownloadState::Available` ([#7059](https://github.com/chatmail/core/pull/7059)).
### Tests
- Log the number of the test account if there are multiple alices ([#7087](https://github.com/chatmail/core/pull/7087)).
### CI
- Update Rust to 1.89.0.
### Refactor
- Rename icon-address-contact to icon-unencrypted.
- Skip loading the contact of 1:1 unencrypted chat to show the avatar.
- Chat::is_encrypted(): Make one query instead of two for 1:1 chats.
### Miscellaneous Tasks
- cargo: Bump toml from 0.8.23 to 0.9.4.
- cargo: Bump human-panic from 2.0.2 to 2.0.3.
- deny.toml: Add exception for duplicate toml_datetime 0.6.11 dependency.
- deps: Bump actions/checkout from 4 to 5.
- deps: Bump actions/download-artifact from 4 to 5.
## [2.10.0] - 2025-08-04
### Features / Changes
- Also lookup key contacts in lookup_id_by_addr() ([#7073](https://github.com/chatmail/core/pull/7073)).
### Miscellaneous Tasks
- cargo: Bump serde_json from 1.0.140 to 1.0.142.
- cargo: Bump bolero from 0.13.3 to 0.13.4.
- cargo: Bump async-channel from 2.3.1 to 2.5.0.
- cargo: Bump hyper-util from 0.1.14 to 0.1.16.
- cargo: Bump criterion from 0.6.0 to 0.7.0.
- cargo: Bump strum from 0.27.1 to 0.27.2.
- cargo: Bump strum_macros from 0.27.1 to 0.27.2.
- Upgrade async-imap to 0.11.1.
## [2.9.0] - 2025-07-31
### Features / Changes
- repl: Add import-vcard and make-vcard commands ([#7048](https://github.com/chatmail/core/pull/7048)).
### Fixes
- Display correct timer value for ephemeral timer changes.
- Get_chat_msgs_ex(): Report local midnight in ChatItem::DayMarker.
### Refactor
- Rename add_or_lookup_key_contacts_by_address_list() to add_or_lookup_key_contacts().
- Don't call add_or_lookup_key_contacts() in advance.
## [2.8.0] - 2025-07-28
### Features / Changes
- Remove ProtectionBroken, make such chats Unprotected ([#7041](https://github.com/chatmail/core/pull/7041)).
### Fixes
- Lookup self by address if there is no fingerprint or gossip.
## [2.7.0] - 2025-07-26
### Features / Changes
- Mimefactory: Order message recipients by time of addition ([#6872](https://github.com/chatmail/core/pull/6872)).
- Put the debug/release build version into the info ([#7034](https://github.com/chatmail/core/pull/7034)).
### Fixes
- Realtime late join ([#6869](https://github.com/chatmail/core/pull/6869)).
- Do not fail to upgrade if the verifier of a contact doesn't exist anymore ([#7044](https://github.com/chatmail/core/pull/7044)).
### Tests
- Add regression test for verification-gossiping crash ([#7033](https://github.com/chatmail/core/pull/7033)).
## [2.6.0] - 2025-07-23
### Fixes
@@ -6673,9 +6533,3 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[2.4.0]: https://github.com/chatmail/core/compare/v2.3.0..v2.4.0
[2.5.0]: https://github.com/chatmail/core/compare/v2.4.0..v2.5.0
[2.6.0]: https://github.com/chatmail/core/compare/v2.5.0..v2.6.0
[2.7.0]: https://github.com/chatmail/core/compare/v2.6.0..v2.7.0
[2.8.0]: https://github.com/chatmail/core/compare/v2.7.0..v2.8.0
[2.9.0]: https://github.com/chatmail/core/compare/v2.8.0..v2.9.0
[2.10.0]: https://github.com/chatmail/core/compare/v2.9.0..v2.10.0
[2.11.0]: https://github.com/chatmail/core/compare/v2.10.0..v2.11.0
[2.12.0]: https://github.com/chatmail/core/compare/v2.11.0..v2.12.0

172
Cargo.lock generated
View File

@@ -229,9 +229,9 @@ dependencies = [
[[package]]
name = "async-channel"
version = "2.5.0"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
dependencies = [
"concurrent-queue",
"event-listener-strategy",
@@ -268,11 +268,11 @@ dependencies = [
[[package]]
name = "async-imap"
version = "0.11.1"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8da885da5980f3934831e6370445c0e0e44ef251d7792308b39e908915a41d09"
checksum = "8e9f9a9c94a403cf46aa2b4cecbceefc6e4284441ebbeca79b80f3bab4394458"
dependencies = [
"async-channel 2.5.0",
"async-channel 2.3.1",
"async-compression",
"base64",
"bytes",
@@ -535,9 +535,9 @@ dependencies = [
[[package]]
name = "bolero"
version = "0.13.4"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ff44d278fc0062c95327087ed96b3d256906d1d8f579e534a3de8d6b386913a"
checksum = "4e913ed74716cd68dc5be41c702327b1cc4ffc8f0b55945ae46fb015777007eb"
dependencies = [
"bolero-afl",
"bolero-engine",
@@ -561,9 +561,9 @@ dependencies = [
[[package]]
name = "bolero-engine"
version = "0.13.4"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dca199170a7c92c669c1019f9219a316b66bcdcfa4b36cac5a460a4c1a851aba"
checksum = "05cae8c41807b046bb7005f52fa60c8f67787c1bf272242f0b84224853e04ceb"
dependencies = [
"anyhow",
"bolero-generator",
@@ -575,9 +575,9 @@ dependencies = [
[[package]]
name = "bolero-generator"
version = "0.13.5"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98a5782f2650f80d533f58ec339c6dce4cc5428f9c2755894f98156f52af81f2"
checksum = "8e3ac7405f187921256faa515fa05ae02521103582a9d938410cefabe3a9a172"
dependencies = [
"bolero-generator-derive",
"either",
@@ -588,14 +588,14 @@ dependencies = [
[[package]]
name = "bolero-generator-derive"
version = "0.13.4"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a21a3b022507b9edd2050caf370d945e398c1a7c8455531220fa3968c45d29e"
checksum = "9c56c2f8c1c0707d678bebb36168cfd523c45927bb8d9cb7567d3578fa428cbd"
dependencies = [
"proc-macro-crate 2.0.0",
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 1.0.109",
]
[[package]]
@@ -929,17 +929,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "colorutils-rs"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "103c2458789cd7b46e6ed7c7ba1bf969b6569c902e3732843c55962c53eac686"
dependencies = [
"erydanos",
"half",
"num-traits",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
@@ -1034,16 +1023,16 @@ dependencies = [
[[package]]
name = "criterion"
version = "0.7.0"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1c047a62b0cc3e145fa84415a3191f628e980b194c2755aa12300a4e6cbd928"
checksum = "3bf7af66b0989381bd0be551bd7cc91912a655a58c6918420c9527b1fd8b4679"
dependencies = [
"anes",
"cast",
"ciborium",
"clap",
"criterion-plot",
"itertools",
"itertools 0.13.0",
"num-traits",
"oorandom",
"plotters",
@@ -1058,12 +1047,12 @@ dependencies = [
[[package]]
name = "criterion-plot"
version = "0.6.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338"
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
dependencies = [
"cast",
"itertools",
"itertools 0.10.5",
]
[[package]]
@@ -1296,11 +1285,11 @@ dependencies = [
[[package]]
name = "deltachat"
version = "2.12.0"
version = "2.6.0"
dependencies = [
"anyhow",
"async-broadcast",
"async-channel 2.5.0",
"async-channel 2.3.1",
"async-imap",
"async-native-tls",
"async-smtp",
@@ -1310,7 +1299,6 @@ dependencies = [
"brotli",
"bytes",
"chrono",
"colorutils-rs",
"criterion",
"data-encoding",
"deltachat-contact-tools",
@@ -1354,6 +1342,7 @@ dependencies = [
"ratelimit",
"regex",
"rusqlite",
"rust-hsluv",
"rustls",
"rustls-pki-types",
"sanitize-filename",
@@ -1364,8 +1353,8 @@ dependencies = [
"sha2",
"shadowsocks",
"smallvec",
"strum 0.27.2",
"strum_macros 0.27.2",
"strum 0.27.1",
"strum_macros 0.27.1",
"tagger",
"tempfile",
"testdir",
@@ -1406,10 +1395,10 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "2.12.0"
version = "2.6.0"
dependencies = [
"anyhow",
"async-channel 2.5.0",
"async-channel 2.3.1",
"base64",
"deltachat",
"deltachat-contact-tools",
@@ -1428,7 +1417,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "2.12.0"
version = "2.6.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1444,7 +1433,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "2.12.0"
version = "2.6.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1473,7 +1462,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "2.12.0"
version = "2.6.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1906,15 +1895,6 @@ version = "3.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f"
[[package]]
name = "erydanos"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cbdc4987ed8e9ece64845393c2d53596b3a4ccbfb3948d799d58f6450e89fb1"
dependencies = [
"num-traits",
]
[[package]]
name = "escaper"
version = "0.1.1"
@@ -1943,9 +1923,9 @@ dependencies = [
[[package]]
name = "event-listener-strategy"
version = "0.5.4"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2"
dependencies = [
"event-listener 5.4.0",
"pin-project-lite",
@@ -2352,9 +2332,9 @@ dependencies = [
[[package]]
name = "half"
version = "2.6.0"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9"
checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e"
dependencies = [
"cfg-if",
"crunchy",
@@ -2577,9 +2557,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "human-panic"
version = "2.0.3"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac63a746b187e95d51fe16850eb04d1cfef203f6af98e6c405a6f262ad3df00a"
checksum = "80b84a66a325082740043a6c28bbea400c129eac0d3a27673a1de971e44bf1f7"
dependencies = [
"backtrace",
"os_info",
@@ -2639,9 +2619,9 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.16"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e"
checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb"
dependencies = [
"bytes",
"futures-channel",
@@ -3039,7 +3019,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ca43045ceb44b913369f417d56323fb1628ebf482ab4c1e9360e81f1b58cbc2"
dependencies = [
"anyhow",
"async-channel 2.5.0",
"async-channel 2.3.1",
"bytes",
"derive_more 1.0.0",
"ed25519-dalek",
@@ -3189,6 +3169,15 @@ dependencies = [
"z32",
]
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.13.0"
@@ -3374,9 +3363,9 @@ checksum = "9106e1d747ffd48e6be5bb2d97fa706ed25b144fbee4d5c02eae110cd8d6badd"
[[package]]
name = "mail-builder"
version = "0.4.4"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "900998f307338c4013a28ab14d760b784067324b164448c6d98a89e44810473b"
checksum = "0926cff74776d4af100a95c90a6649486659526ce638bee6648ecc9c41051810"
[[package]]
name = "mailparse"
@@ -5079,6 +5068,12 @@ dependencies = [
"smallvec",
]
[[package]]
name = "rust-hsluv"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efe2374f2385cdd8755a446f80b2a646de603c9d8539ca38734879b5c71e378b"
[[package]]
name = "rustc-demangle"
version = "0.1.24"
@@ -5398,9 +5393,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.142"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa",
"memchr",
@@ -5410,9 +5405,9 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "1.0.0"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
"serde",
]
@@ -5731,9 +5726,9 @@ dependencies = [
[[package]]
name = "strum"
version = "0.27.2"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
[[package]]
name = "strum_macros"
@@ -5750,13 +5745,14 @@ dependencies = [
[[package]]
name = "strum_macros"
version = "0.27.2"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.104",
]
@@ -6190,17 +6186,14 @@ dependencies = [
[[package]]
name = "toml"
version = "0.9.4"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41ae868b5a0f67631c14589f7e250c1ea2c574ee5ba21c6c8dd4b1485705a5a1"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime 0.7.0",
"toml_parser",
"toml_writer",
"winnow 0.7.11",
"toml_datetime",
"toml_edit 0.22.27",
]
[[package]]
@@ -6208,12 +6201,6 @@ name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
[[package]]
name = "toml_datetime"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3"
dependencies = [
"serde",
]
@@ -6225,7 +6212,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81"
dependencies = [
"indexmap",
"toml_datetime 0.6.11",
"toml_datetime",
"winnow 0.5.40",
]
@@ -6236,32 +6223,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"toml_datetime 0.6.11",
"serde",
"serde_spanned",
"toml_datetime",
"toml_write",
"winnow 0.7.11",
]
[[package]]
name = "toml_parser"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30"
dependencies = [
"winnow 0.7.11",
]
[[package]]
name = "toml_write"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "toml_writer"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64"
[[package]]
name = "tower"
version = "0.5.2"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "2.12.0"
version = "2.6.0"
edition = "2024"
license = "MPL-2.0"
rust-version = "1.85"
@@ -44,16 +44,14 @@ ratelimit = { path = "./deltachat-ratelimit" }
anyhow = { workspace = true }
async-broadcast = "0.7.2"
async-channel = { workspace = true }
async-imap = { version = "0.11.1", default-features = false, features = ["runtime-tokio", "compress"] }
async-imap = { version = "0.11.0", default-features = false, features = ["runtime-tokio", "compress"] }
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
base64 = { workspace = true }
blake3 = "1.8.2"
brotli = { version = "8", default-features=false, features = ["std"] }
bytes = "1"
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
colorutils-rs = { version = "0.7.5", default-features = false }
data-encoding = "2.9.0"
escaper = "0.1"
fast-socks5 = "0.10"
@@ -65,13 +63,13 @@ hickory-resolver = "0.25.2"
http-body-util = "0.1.3"
humansize = "2"
hyper = "1"
hyper-util = "0.1.16"
hyper-util = "0.1.14"
image = { version = "0.25.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh-gossip = { version = "0.35", default-features = false, features = ["net"] }
iroh = { version = "0.35", default-features = false }
kamadak-exif = "0.6.1"
libc = { workspace = true }
mail-builder = { version = "0.4.4", default-features = false }
mail-builder = { version = "0.4.3", default-features = false }
mailparse = { workspace = true }
mime = "0.3.17"
num_cpus = "1.17"
@@ -87,6 +85,7 @@ quoted_printable = "0.5"
rand = { workspace = true }
regex = { workspace = true }
rusqlite = { workspace = true, features = ["sqlcipher"] }
rust-hsluv = "0.1"
rustls-pki-types = "1.12.0"
rustls = { version = "0.23.22", default-features = false }
sanitize-filename = { workspace = true }
@@ -108,15 +107,16 @@ tokio-stream = { version = "0.1.17", features = ["fs"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
tokio-util = { workspace = true }
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
toml = "0.9"
toml = "0.8"
tracing = "0.1.41"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
webpki-roots = "0.26.8"
blake3 = "1.8.2"
[dev-dependencies]
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
criterion = { version = "0.7.0", features = ["async_tokio"] }
criterion = { version = "0.6.0", features = ["async_tokio"] }
futures-lite = { workspace = true }
log = { workspace = true }
nu-ansi-term = { workspace = true }
@@ -175,7 +175,7 @@ harness = false
[workspace.dependencies]
anyhow = "1"
async-channel = "2.5.0"
async-channel = "2.3.1"
base64 = "0.22"
chrono = { version = "0.4.41", default-features = false }
deltachat-contact-tools = { path = "deltachat-contact-tools" }

View File

@@ -80,26 +80,18 @@ Connect to your mail server (if already configured):
> connect
```
Export your public key to a vCard file:
```
> make-vcard my.vcard 1
```
Create contacts by address or vCard file:
Create a contact:
```
> addcontact yourfriends@email.org
> import-vcard key-contact.vcard
```
List contacts:
```
> listcontacts
Contact#Contact#11: key-contact@email.org <key-contact@email.org>
Contact#Contact#Self: Me √ <your@email.org>
2 key contacts.
1 key contacts.
Contact#Contact#10: yourfriends@email.org <yourfriends@email.org>
1 address contacts.
```

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "2.12.0"
version = "2.6.0"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"

View File

@@ -1332,14 +1332,12 @@ dc_msg_t* dc_get_draft (dc_context_t* context, uint32_t ch
* Optionally, some special markers added to the ID array may help to
* implement virtual lists.
*
* To get the concrete time of the message, use dc_array_get_timestamp().
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The chat ID of which the messages IDs should be queried.
* @param flags If set to DC_GCM_ADDDAYMARKER, the marker DC_MSG_ID_DAYMARKER will
* be added before each day (regarding the local timezone). Set this to 0 if you do not want this behaviour.
* The day marker timestamp is the midnight one for the corresponding (following) day in the local timezone.
* To get the concrete time of the marker, use dc_array_get_timestamp().
* If set to DC_GCM_INFO_ONLY, only system messages will be returned, can be combined with DC_GCM_ADDDAYMARKER.
* @param marker1before Deprecated, set this to 0.
* @return Array of message IDs, must be dc_array_unref()'d when no longer used.
@@ -2089,19 +2087,9 @@ int dc_may_be_valid_addr (const char* addr);
/**
* Looks up a known and unblocked contact with a given e-mail address.
* Check if an e-mail address belongs to a known and unblocked contact.
* To get a list of all known and unblocked contacts, use dc_get_contacts().
*
* **POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
* (e.g. an address-contact and a key-contact),
* this looks up the most recently seen contact,
* i.e. which contact is returned depends on which contact last sent a message.
* If the user just clicked on a mailto: link, then this is the best thing you can do.
* But **DO NOT** internally represent contacts by their email address
* and do not use this function to look them up;
* otherwise this function will sometimes look up the wrong contact.
* Instead, you should internally represent contacts by their ids.
*
* To validate an e-mail address independently of the contact database
* use dc_may_be_valid_addr().
*
@@ -2123,13 +2111,6 @@ uint32_t dc_lookup_contact_id_by_addr (dc_context_t* context, const char*
* To add a number of contacts, see dc_add_address_book() which is much faster for adding
* a bunch of addresses.
*
* This will always create or look up an address-contact,
* i.e. a contact identified by an email address,
* with all messages sent to and from this contact being unencrypted.
* If the user just clicked on an email address,
* you should first check `lookup_contact_id_by_addr`,
* and only if there is no contact yet, call this function here.
*
* May result in a #DC_EVENT_CONTACTS_CHANGED event.
*
* @memberof dc_context_t
@@ -7598,18 +7579,6 @@ void dc_event_unref(dc_event_t* event);
/// `%2$s` will be replaced by name and address of the contact.
#define DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER 157
/// "You set message deletion timer to 1 year."
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_1_YEAR_BY_YOU 158
/// "Message deletion timer is set to 1 year by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_1_YEAR_BY_OTHER 159
/// "Scan to set up second device for %1$s"
///
/// `%1$s` will be replaced by name and address of the account.

View File

@@ -375,7 +375,7 @@ pub unsafe extern "C" fn dc_get_connectivity(context: *const dc_context_t) -> li
return 0;
}
let ctx = &*context;
ctx.get_connectivity() as u32 as libc::c_int
block_on(ctx.get_connectivity()) as u32 as libc::c_int
}
#[no_mangle]

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "2.12.0"
version = "2.6.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
license = "MPL-2.0"

View File

@@ -985,7 +985,9 @@ impl CommandApi {
/// Create a new unencrypted group chat.
///
/// Same as [`Self::create_group_chat`], but the chat is unencrypted and can only have
/// address-contacts.
/// address-contacts. NB: Chats with similar names and the same members are merged on other
/// devices, but usually users don't create such chats and look up the existing one instead, so
/// chat split on the first device is acceptable.
async fn create_group_chat_unencrypted(&self, account_id: u32, name: String) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
chat::create_group_ex(&ctx, None, &name)
@@ -1227,10 +1229,8 @@ impl CommandApi {
}
/// Returns all messages of a particular chat.
///
/// * `add_daymarker` - If `true`, add day markers as `DC_MSG_ID_DAYMARKER` to the result,
/// e.g. [1234, 1237, 9, 1239]. The day marker timestamp is the midnight one for the
/// corresponding (following) day in the local timezone.
/// If `add_daymarker` is `true`, it will return them as
/// `DC_MSG_ID_DAYMARKER`, e.g. [1234, 1237, 9, 1239].
async fn get_message_ids(
&self,
account_id: u32,
@@ -1473,14 +1473,7 @@ impl CommandApi {
/// Add a single contact as a result of an explicit user action.
///
/// This will always create or look up an address-contact,
/// i.e. a contact identified by an email address,
/// with all messages sent to and from this contact being unencrypted.
/// If the user just clicked on an email address,
/// you should first check [`Self::lookup_contact_id_by_addr`]/`lookupContactIdByAddr.`,
/// and only if there is no contact yet, call this function here.
///
/// Returns contact id of the created or existing contact.
/// Returns contact id of the created or existing contact
async fn create_contact(
&self,
account_id: u32,
@@ -1630,19 +1623,9 @@ impl CommandApi {
Contact::get_encrinfo(&ctx, ContactId::new(contact_id)).await
}
/// Looks up a known and unblocked contact with a given e-mail address.
/// Check if an e-mail address belongs to a known and unblocked contact.
/// To get a list of all known and unblocked contacts, use contacts_get_contacts().
///
/// **POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
/// (e.g. an address-contact and a key-contact),
/// this looks up the most recently seen contact,
/// i.e. which contact is returned depends on which contact last sent a message.
/// If the user just clicked on a mailto: link, then this is the best thing you can do.
/// But **DO NOT** internally represent contacts by their email address
/// and do not use this function to look them up;
/// otherwise this function will sometimes look up the wrong contact.
/// Instead, you should internally represent contacts by their ids.
///
/// To validate an e-mail address independently of the contact database
/// use check_email_validity().
async fn lookup_contact_id_by_addr(
@@ -1908,7 +1891,7 @@ impl CommandApi {
/// If the connectivity changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
async fn get_connectivity(&self, account_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
Ok(ctx.get_connectivity() as u32)
Ok(ctx.get_connectivity().await as u32)
}
/// Get an overview of the current connectivity, and possibly more statistics.

View File

@@ -71,9 +71,7 @@ pub struct FullChat {
fresh_message_counter: usize,
// is_group - please check over chat.type in frontend instead
is_contact_request: bool,
/// Deprecated 2025-07. Chats protection cannot break any longer.
is_protection_broken: bool,
is_protection_broken: bool, // deprecated 2025-07
is_device_chat: bool,
self_in_group: bool,
is_muted: bool,
@@ -218,9 +216,7 @@ pub struct BasicChat {
is_self_talk: bool,
color: String,
is_contact_request: bool,
/// Deprecated 2025-07. Chats protection cannot break any longer.
is_protection_broken: bool,
is_device_chat: bool,
is_muted: bool,
}

View File

@@ -54,5 +54,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "2.12.0"
"version": "2.6.0"
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "2.12.0"
version = "2.6.0"
license = "MPL-2.0"
edition = "2021"
repository = "https://github.com/chatmail/core"

View File

@@ -403,8 +403,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
block <contact-id>\n\
unblock <contact-id>\n\
listblocked\n\
import-vcard <file>\n\
make-vcard <file> <contact-id> [contact-id ...]\n\
======================================Misc.==\n\
getqr [<chat-id>]\n\
getqrsvg [<chat-id>]\n\
@@ -1220,24 +1218,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
log_contactlist(&context, &contacts).await?;
println!("{} blocked contacts.", contacts.len());
}
"import-vcard" => {
ensure!(!arg1.is_empty(), "Argument <file> missing.");
let vcard_content = fs::read_to_string(&arg1.to_string()).await?;
let contacts = import_vcard(&context, &vcard_content).await?;
println!("vCard contacts imported:");
log_contactlist(&context, &contacts).await?;
}
"make-vcard" => {
ensure!(!arg1.is_empty(), "Argument <file> missing.");
ensure!(!arg2.is_empty(), "Argument <contact-id> missing.");
let mut contact_ids = vec![];
for x in arg2.split_whitespace() {
contact_ids.push(ContactId::new(x.parse()?))
}
let vcard_content = make_vcard(&context, &contact_ids).await?;
fs::write(&arg1.to_string(), vcard_content).await?;
println!("vCard written to: {arg1}");
}
"checkqr" => {
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
let qr = check_qr(&context, arg1).await?;

View File

@@ -232,7 +232,7 @@ const MESSAGE_COMMANDS: [&str; 10] = [
"delmsg",
"react",
];
const CONTACT_COMMANDS: [&str; 9] = [
const CONTACT_COMMANDS: [&str; 7] = [
"listcontacts",
"addcontact",
"contactinfo",
@@ -240,8 +240,6 @@ const CONTACT_COMMANDS: [&str; 9] = [
"block",
"unblock",
"listblocked",
"import-vcard",
"make-vcard",
];
const MISC_COMMANDS: [&str; 14] = [
"getqr",

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "2.12.0"
version = "2.6.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",

View File

@@ -185,21 +185,7 @@ class Account:
return Contact(self, contact_id)
def get_contact_by_addr(self, address: str) -> Optional[Contact]:
"""Looks up a known and unblocked contact with a given e-mail address.
To get a list of all known and unblocked contacts, use contacts_get_contacts().
**POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
(e.g. an address-contact and a key-contact),
this looks up the most recently seen contact,
i.e. which contact is returned depends on which contact last sent a message.
If the user just clicked on a mailto: link, then this is the best thing you can do.
But **DO NOT** internally represent contacts by their email address
and do not use this function to look them up;
otherwise this function will sometimes look up the wrong contact.
Instead, you should internally represent contacts by their ids.
To validate an e-mail address independently of the contact database
use check_email_validity()."""
"""Check if an e-mail address belongs to a known and unblocked contact."""
contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address)
return contact_id and Contact(self, contact_id)

View File

@@ -171,10 +171,7 @@ def test_account(acfactory) -> None:
assert alice.get_size()
assert alice.is_configured()
assert not alice.get_avatar()
# get_contact_by_addr() can lookup a key contact by address:
bob_contact = alice.get_contact_by_addr(bob_addr).get_snapshot()
assert bob_contact.display_name == "Bob"
assert bob_contact.is_key_contact
assert alice.get_contact_by_addr(bob_addr) is None # There is no address-contact, only key-contact
assert alice.get_contacts()
assert alice.get_contacts(snapshot=True)
assert alice.self_contact

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "2.12.0"
version = "2.6.0"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"

View File

@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "2.12.0"
"version": "2.6.0"
}

View File

@@ -48,7 +48,6 @@ skip = [
{ name = "syn", version = "1.0.109" },
{ name = "thiserror-impl", version = "1.0.69" },
{ name = "thiserror", version = "1.0.69" },
{ name = "toml_datetime", version = "0.6.11" },
{ name = "wasi", version = "0.11.0+wasi-snapshot-preview1" },
{ name = "windows" },
{ name = "windows_aarch64_gnullvm" },

View File

@@ -6,7 +6,7 @@ edition = "2021"
license = "MPL-2.0"
[dev-dependencies]
bolero = "0.13.4"
bolero = "0.13.3"
[dependencies]
mailparse = { workspace = true }

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "2.12.0"
version = "2.6.0"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"
requires-python = ">=3.8"

View File

@@ -330,21 +330,7 @@ class Account:
return bool(lib.dc_delete_contact(self._dc_context, contact_id))
def get_contact_by_addr(self, email: str) -> Optional[Contact]:
"""Looks up a known and unblocked contact with a given e-mail address.
To get a list of all known and unblocked contacts, use contacts_get_contacts().
**POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
(e.g. an address-contact and a key-contact),
this looks up the most recently seen contact,
i.e. which contact is returned depends on which contact last sent a message.
If the user just clicked on a mailto: link, then this is the best thing you can do.
But **DO NOT** internally represent contacts by their email address
and do not use this function to look them up;
otherwise this function will sometimes look up the wrong contact.
Instead, you should internally represent contacts by their ids.
To validate an e-mail address independently of the contact database
use check_email_validity()."""
"""get a contact for the email address or None if it's blocked or doesn't exist."""
_, addr = parseaddr(email)
addr = as_dc_charpointer(addr)
contact_id = lib.dc_lookup_contact_id_by_addr(self._dc_context, addr)

View File

@@ -8,6 +8,7 @@ from typing import Optional, Union
from . import const, props
from .capi import ffi, lib
from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer
from .reactions import Reactions
class Message:
@@ -163,6 +164,17 @@ class Message:
),
)
def send_reaction(self, reaction: str):
"""Send a reaction to message and return the resulting Message instance."""
msg_id = lib.dc_send_reaction(self.account._dc_context, self.id, as_dc_charpointer(reaction))
if msg_id == 0:
raise ValueError("reaction could not be send")
return Message.from_db(self.account, msg_id)
def get_reactions(self) -> Reactions:
"""Get :class:`deltachat.reactions.Reactions` to the message."""
return Reactions.from_msg(self)
def is_system_message(self):
"""return True if this message is a system/info message."""
return bool(lib.dc_msg_is_info(self._dc_msg))

View File

@@ -0,0 +1,43 @@
"""The Reactions object."""
from .capi import ffi, lib
from .cutil import from_dc_charpointer, iter_array
class Reactions:
"""Reactions object.
You obtain instances of it through :class:`deltachat.message.Message`.
"""
def __init__(self, account, dc_reactions) -> None:
assert isinstance(account._dc_context, ffi.CData)
assert isinstance(dc_reactions, ffi.CData)
assert dc_reactions != ffi.NULL
self.account = account
self._dc_reactions = dc_reactions
def __repr__(self) -> str:
return f"<Reactions dc_reactions={self._dc_reactions}>"
@classmethod
def from_msg(cls, msg):
assert msg.id > 0
return cls(
msg.account,
ffi.gc(lib.dc_get_msg_reactions(msg.account._dc_context, msg.id), lib.dc_reactions_unref),
)
def get_contacts(self) -> list:
"""Get list of contacts reacted to the message.
:returns: list of :class:`deltachat.contact.Contact` objects for this reaction.
"""
from .contact import Contact
dc_array = ffi.gc(lib.dc_reactions_get_contacts(self._dc_reactions), lib.dc_array_unref)
return list(iter_array(dc_array, lambda x: Contact(self.account, x)))
def get_by_contact(self, contact) -> str:
"""Get a string containing space-separated reactions of a single :class:`deltachat.contact.Contact`."""
return from_dc_charpointer(lib.dc_reactions_get_by_contact_id(self._dc_reactions, contact.id))

View File

@@ -1 +1 @@
2025-08-26
2025-07-23

View File

@@ -7,7 +7,7 @@ set -euo pipefail
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.89.0
RUST_VERSION=1.88.0
ARCH="$(uname -m)"
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu

View File

@@ -94,12 +94,14 @@ pub enum ProtectionStatus {
///
/// All members of the chat must be verified.
Protected = 1,
// `2` was never used as a value.
// Chats don't break in Core v2 anymore. Chats with broken protection existing before the
// key-contacts migration are treated as `Unprotected`.
//
// ProtectionBroken = 3,
/// The chat was protected, but now a new message came in
/// which was not encrypted / signed correctly.
/// The user has to confirm that this is OK.
///
/// We only do this in 1:1 chats; in group chats, the chat just
/// stays protected.
ProtectionBroken = 3, // `2` was never used as a value.
}
/// The reason why messages cannot be sent to the chat.
@@ -116,6 +118,10 @@ pub(crate) enum CantSendReason {
/// The chat is a contact request, it needs to be accepted before sending a message.
ContactRequest,
/// Deprecated. The chat was protected, but now a new message came in
/// which was not encrypted / signed correctly.
ProtectionBroken,
/// Mailing list without known List-Post header.
ReadOnlyMailingList,
@@ -138,6 +144,10 @@ impl fmt::Display for CantSendReason {
f,
"contact request chat should be accepted before sending messages"
),
Self::ProtectionBroken => write!(
f,
"accept that the encryption isn't verified anymore before sending messages"
),
Self::ReadOnlyMailingList => {
write!(f, "mailing list does not have a know post address")
}
@@ -469,6 +479,16 @@ impl ChatId {
let chat = Chat::load_from_db(context, self).await?;
match chat.typ {
Chattype::Single
if chat.blocked == Blocked::Not
&& chat.protected == ProtectionStatus::ProtectionBroken =>
{
// The protection was broken, then the user clicked 'Accept'/'OK',
// so, now we want to set the status to Unprotected again:
chat.id
.inner_set_protection(context, ProtectionStatus::Unprotected)
.await?;
}
Chattype::Single | Chattype::Group | Chattype::OutBroadcast | Chattype::InBroadcast => {
// User has "created a chat" with all these contacts.
//
@@ -525,7 +545,7 @@ impl ChatId {
| Chattype::InBroadcast => {}
Chattype::Mailinglist => bail!("Cannot protect mailing lists"),
},
ProtectionStatus::Unprotected => {}
ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {}
};
context
@@ -568,6 +588,7 @@ impl ChatId {
let cmd = match protect {
ProtectionStatus::Protected => SystemMessage::ChatProtectionEnabled,
ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled,
ProtectionStatus::ProtectionBroken => SystemMessage::ChatProtectionDisabled,
};
add_info_msg_with_cmd(
context,
@@ -1679,6 +1700,12 @@ impl Chat {
return Ok(Some(reason));
}
}
if self.is_protection_broken() {
let reason = ProtectionBroken;
if !skip_fn(&reason) {
return Ok(Some(reason));
}
}
if self.is_mailing_list() && self.get_mailinglist_addr().is_none_or_empty() {
let reason = ReadOnlyMailingList;
if !skip_fn(&reason) {
@@ -1771,12 +1798,6 @@ impl Chat {
return Ok(Some(get_device_icon(context).await?));
} else if self.is_self_talk() {
return Ok(Some(get_saved_messages_icon(context).await?));
} else if !self.is_encrypted(context).await? {
// This is an unencrypted chat, show a special avatar that marks it as such.
return Ok(Some(get_abs_path(
context,
Path::new(&get_unencrypted_icon(context).await?),
)));
} else if self.typ == Chattype::Single {
// For 1:1 chats, we always use the same avatar as for the contact
// This is before the `self.is_encrypted()` check, because that function
@@ -1786,6 +1807,12 @@ impl Chat {
let contact = Contact::get_by_id(context, *contact_id).await?;
return contact.get_profile_image(context).await;
}
} else if !self.is_encrypted(context).await? {
// This is an address-contact chat, show a special avatar that marks it as such
return Ok(Some(get_abs_path(
context,
Path::new(&get_address_contact_icon(context).await?),
)));
} else if let Some(image_rel) = self.param.get(Param::ProfileImage) {
// Load the group avatar, or the device-chat / saved-messages icon
if !image_rel.is_empty() {
@@ -1797,9 +1824,8 @@ impl Chat {
/// Returns chat avatar color.
///
/// For 1:1 chats, the color is calculated from the contact's address
/// for address-contacts and from the OpenPGP key fingerprint for key-contacts.
/// For group chats the color is calculated from the grpid, if present, or the chat name.
/// For 1:1 chats, the color is calculated from the contact's address.
/// For group chats the color is calculated from the chat name.
pub async fn get_color(&self, context: &Context) -> Result<u32> {
let mut color = 0;
@@ -1810,8 +1836,6 @@ impl Chat {
color = contact.get_color();
}
}
} else if !self.grpid.is_empty() {
color = str_to_color(&self.grpid);
} else {
color = str_to_color(&self.name);
}
@@ -1889,25 +1913,16 @@ impl Chat {
let is_encrypted = self.is_protected()
|| match self.typ {
Chattype::Single => {
match context
.sql
.query_row_optional(
"SELECT cc.contact_id, c.fingerprint<>''
FROM chats_contacts cc LEFT JOIN contacts c
ON c.id=cc.contact_id
WHERE cc.chat_id=?
",
(self.id,),
|row| {
let id: ContactId = row.get(0)?;
let is_key: bool = row.get(1)?;
Ok((id, is_key))
},
)
.await?
{
Some((id, is_key)) => is_key || id == ContactId::DEVICE,
None => true,
let chat_contact_ids = get_chat_contacts(context, self.id).await?;
if let Some(contact_id) = chat_contact_ids.first() {
if *contact_id == ContactId::DEVICE {
true
} else {
let contact = Contact::get_by_id(context, *contact_id).await?;
contact.is_key_contact()
}
} else {
true
}
}
Chattype::Group => {
@@ -1920,9 +1935,25 @@ impl Chat {
Ok(is_encrypted)
}
/// Deprecated 2025-07. Returns false.
/// Deprecated 2025-07. Returns true if the chat was protected, and then an incoming message broke this protection.
///
/// This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag,
/// otherwise it will return false for all chats.
///
/// 1:1 chats are automatically set as protected when a contact is verified.
/// When a message comes in that is not encrypted / signed correctly,
/// the chat is automatically set as unprotected again.
/// `is_protection_broken()` will return true until `chat_id.accept()` is called.
///
/// The UI should let the user confirm that this is OK with a message like
/// `Bob sent a message from another device. Tap to learn more`
/// and then call `chat_id.accept()`.
pub fn is_protection_broken(&self) -> bool {
false
match self.protected {
ProtectionStatus::Protected => false,
ProtectionStatus::Unprotected => false,
ProtectionStatus::ProtectionBroken => true,
}
}
/// Returns true if location streaming is enabled in the chat.
@@ -2502,13 +2533,11 @@ pub(crate) async fn get_archive_icon(context: &Context) -> Result<PathBuf> {
.await
}
/// Returns path to the icon
/// indicating unencrypted chats and address-contacts.
pub(crate) async fn get_unencrypted_icon(context: &Context) -> Result<PathBuf> {
pub(crate) async fn get_address_contact_icon(context: &Context) -> Result<PathBuf> {
get_asset_icon(
context,
"icon-unencrypted",
include_bytes!("../assets/icon-unencrypted.png"),
"icon-address-contact",
include_bytes!("../assets/icon-address-contact.png"),
)
.await
}
@@ -2918,7 +2947,7 @@ async fn prepare_send_msg(
let mut chat = Chat::load_from_db(context, chat_id).await?;
let skip_fn = |reason: &CantSendReason| match reason {
CantSendReason::ContactRequest => {
CantSendReason::ProtectionBroken | CantSendReason::ContactRequest => {
// Allow securejoin messages, they are supposed to repair the verification.
// If the chat is a contact request, let the user accept it later.
msg.param.get_cmd() == SystemMessage::SecurejoinMessage
@@ -2988,10 +3017,6 @@ async fn prepare_send_msg(
/// Constructs jobs for sending a message and inserts them into the appropriate table.
///
/// Updates the message `GuaranteeE2ee` parameter and persists it
/// in the database depending on whether the message
/// is added to the outgoing queue as encrypted or not.
///
/// Returns row ids if `smtp` table jobs were created or an empty `Vec` otherwise.
///
/// The caller has to interrupt SMTP loop or otherwise process new rows.
@@ -3085,20 +3110,13 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
}
}
if rendered_msg.is_encrypted {
if rendered_msg.is_encrypted && !needs_encryption {
msg.param.set_int(Param::GuaranteeE2ee, 1);
} else {
msg.param.remove(Param::GuaranteeE2ee);
msg.update_param(context).await?;
}
msg.subject.clone_from(&rendered_msg.subject);
context
.sql
.execute(
"UPDATE msgs SET subject=?, param=? WHERE id=?",
(&msg.subject, msg.param.to_string(), msg.id),
)
.await?;
msg.subject.clone_from(&rendered_msg.subject);
msg.update_subject(context).await?;
let chunk_size = context.get_max_smtp_rcpt_to().await?;
let trans_fn = |t: &mut rusqlite::Transaction| {
let mut row_ids = Vec::<i64>::new();
@@ -3348,11 +3366,10 @@ pub async fn get_chat_msgs_ex(
for (ts, curr_id) in sorted_rows {
if add_daymarker {
let curr_local_timestamp = ts + cnv_to_local;
let secs_in_day = 86400;
let curr_day = curr_local_timestamp / secs_in_day;
let curr_day = curr_local_timestamp / 86400;
if curr_day != last_day {
ret.push(ChatItem::DayMarker {
timestamp: curr_day * secs_in_day - cnv_to_local,
timestamp: curr_day * 86400, // Convert day back to Unix timestamp
});
last_day = curr_day;
}
@@ -3695,18 +3712,17 @@ pub async fn create_group_chat(
///
/// * `encryption` - If `Some`, the chat is encrypted (with key-contacts) and can be protected.
/// * `name` - Chat name.
///
/// NB: Unencrypted chats with similar names and the same members are merged on other devices, but
/// usually users don't create such chats and look up the existing one instead, so chat split on the
/// first device is acceptable.
pub async fn create_group_ex(
context: &Context,
encryption: Option<ProtectionStatus>,
name: &str,
) -> Result<ChatId> {
let mut chat_name = sanitize_single_line(name);
if chat_name.is_empty() {
// We can't just fail because the user would lose the work already done in the UI like
// selecting members.
error!(context, "Invalid chat name: {name}.");
chat_name = "".to_string();
}
let chat_name = sanitize_single_line(name);
ensure!(!chat_name.is_empty(), "Invalid chat name");
let grpid = match encryption {
Some(_) => create_id(),
@@ -4511,24 +4527,15 @@ pub(crate) async fn save_copy_in_self_talk(
bail!("message already saved.");
}
let copy_fields = "from_id, to_id, timestamp_rcvd, type, txt,
mime_modified, mime_headers, mime_compressed, mime_in_reply_to, subject, msgrmsg";
let copy_fields = "from_id, to_id, timestamp_sent, timestamp_rcvd, type, txt, \
mime_modified, mime_headers, mime_compressed, mime_in_reply_to, subject, msgrmsg";
let row_id = context
.sql
.insert(
&format!(
"INSERT INTO msgs ({copy_fields},
timestamp_sent,
chat_id, rfc724_mid, state, timestamp, param, starred)
SELECT {copy_fields},
-- Outgoing messages on originating device
-- have timestamp_sent == 0.
-- We copy sort timestamp instead
-- so UIs display the same timestamp
-- for saved and original message.
IIF(timestamp_sent == 0, timestamp, timestamp_sent),
?, ?, ?, ?, ?, ?
FROM msgs WHERE id=?;"
"INSERT INTO msgs ({copy_fields}, chat_id, rfc724_mid, state, timestamp, param, starred) \
SELECT {copy_fields}, ?, ?, ?, ?, ?, ? \
FROM msgs WHERE id=?;"
),
(
dest_chat_id,
@@ -4559,9 +4566,18 @@ pub(crate) async fn save_copy_in_self_talk(
///
/// This is primarily intended to make existing webxdcs available to new chat members.
pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
let mut chat_id = None;
let mut msgs: Vec<Message> = Vec::new();
for msg_id in msg_ids {
let msg = Message::load_from_db(context, *msg_id).await?;
if let Some(chat_id) = chat_id {
ensure!(
chat_id == msg.chat_id,
"messages to resend needs to be in the same chat"
);
} else {
chat_id = Some(msg.chat_id);
}
ensure!(
msg.from_id == ContactId::SELF,
"can resend only own messages"
@@ -4570,7 +4586,16 @@ pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
msgs.push(msg)
}
let Some(chat_id) = chat_id else {
return Ok(());
};
let chat = Chat::load_from_db(context, chat_id).await?;
for mut msg in msgs {
if msg.get_showpadlock() && !chat.is_protected() {
msg.param.remove(Param::GuaranteeE2ee);
msg.update_param(context).await?;
}
match msg.get_state() {
// `get_state()` may return an outdated `OutPending`, so update anyway.
MessageState::OutPending
@@ -4581,21 +4606,16 @@ pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
}
msg_state => bail!("Unexpected message state {msg_state}"),
}
msg.timestamp_sort = create_smeared_timestamp(context);
if create_send_msg_jobs(context, &mut msg).await?.is_empty() {
continue;
}
// Emit the event only after `create_send_msg_jobs`
// because `create_send_msg_jobs` may change the message
// encryption status and call `msg.update_param`.
context.emit_event(EventType::MsgsChanged {
chat_id: msg.chat_id,
msg_id: msg.id,
});
msg.timestamp_sort = create_smeared_timestamp(context);
// note(treefit): only matters if it is the last message in chat (but probably to expensive to check, debounce also solves it)
chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
if create_send_msg_jobs(context, &mut msg).await?.is_empty() {
continue;
}
if msg.viewtype == Viewtype::Webxdc {
let conn_fn = |conn: &mut rusqlite::Connection| {
let range = conn.query_row(

View File

@@ -32,7 +32,7 @@ async fn test_chat_info() {
"archived": false,
"param": "",
"is_sending_locations": false,
"color": 29377,
"color": 35391,
"profile_image": {},
"draft": "",
"is_muted": false,
@@ -1929,31 +1929,19 @@ async fn test_classic_email_chat() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_chat_get_color() -> Result<()> {
let t = TestContext::new().await;
let chat_id = create_group_ex(&t, None, "a chat").await?;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat").await?;
let color1 = Chat::load_from_db(&t, chat_id).await?.get_color(&t).await?;
assert_eq!(color1, 0x613dd7);
assert_eq!(color1, 0x008772);
// upper-/lowercase makes a difference for the colors, these are different groups
// (in contrast to email addresses, where upper-/lowercase is ignored in practise)
let t = TestContext::new().await;
let chat_id = create_group_ex(&t, None, "A CHAT").await?;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "A CHAT").await?;
let color2 = Chat::load_from_db(&t, chat_id).await?.get_color(&t).await?;
assert_ne!(color2, color1);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_chat_get_color_encrypted() -> Result<()> {
let mut tcm = TestContextManager::new();
let t = &tcm.alice().await;
let chat_id = create_group_ex(t, Some(ProtectionStatus::Unprotected), "a chat").await?;
let color1 = Chat::load_from_db(t, chat_id).await?.get_color(t).await?;
set_chat_name(t, chat_id, "A CHAT").await?;
let color2 = Chat::load_from_db(t, chat_id).await?.get_color(t).await?;
assert_eq!(color2, color1);
Ok(())
}
async fn test_sticker(
filename: &str,
bytes: &[u8],
@@ -2292,19 +2280,14 @@ async fn test_only_minimal_data_are_forwarded() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_save_msgs() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat = alice.create_chat(&bob).await;
let sent = alice.send_text(alice_chat.get_id(), "hi, bob").await;
let sent_msg = Message::load_from_db(&alice, sent.sender_msg_id).await?;
assert!(sent_msg.get_saved_msg_id(&alice).await?.is_none());
assert!(sent_msg.get_original_msg_id(&alice).await?.is_none());
let sent_timestamp = sent_msg.get_timestamp();
assert!(sent_timestamp > 0);
SystemTime::shift(Duration::from_secs(60));
let self_chat = alice.get_self_chat().await;
save_msgs(&alice, &[sent.sender_msg_id]).await?;
@@ -2322,8 +2305,6 @@ async fn test_save_msgs() -> Result<()> {
assert_eq!(saved_msg.get_from_id(), ContactId::SELF);
assert_eq!(saved_msg.get_state(), MessageState::OutDelivered);
assert_ne!(saved_msg.rfc724_mid(), sent_msg.rfc724_mid());
let saved_timestamp = saved_msg.get_timestamp();
assert_eq!(saved_timestamp, sent_timestamp);
let sent_msg = Message::load_from_db(&alice, sent.sender_msg_id).await?;
assert_eq!(
@@ -4761,16 +4742,6 @@ async fn test_create_unencrypted_group_chat() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_group_invalid_name() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let chat_id = create_group_ex(alice, None, " ").await?;
let chat = Chat::load_from_db(alice, chat_id).await?;
assert_eq!(chat.get_name(), "");
Ok(())
}
/// Tests that avatar cannot be set in ad hoc groups.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_no_avatar_in_adhoc_chats() -> Result<()> {
@@ -4801,25 +4772,3 @@ async fn test_no_avatar_in_adhoc_chats() -> Result<()> {
Ok(())
}
/// Tests that long group name with non-ASCII characters is correctly received
/// by other members.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_long_group_name() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let group_name = "δδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδ";
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, group_name).await?;
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
let sent = alice
.send_text(alice_chat_id, "Hi! I created a group.")
.await;
let bob_chat_id = bob.recv_msg(&sent).await.chat_id;
let bob_chat = Chat::load_from_db(bob, bob_chat_id).await?;
assert_eq!(bob_chat.name, group_name);
Ok(())
}

View File

@@ -245,6 +245,9 @@ impl Chatlist {
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
};
// Return ProtectionBroken chats also, as that may happen to a verified chat at any
// time. It may be confusing if a chat that is normally in the list disappears
// suddenly. The UI need to deal with that case anyway.
context.sql.query_map(
"SELECT c.id, c.type, c.param, m.id
FROM chats c

View File

@@ -1,39 +1,38 @@
//! Color generation.
//! Implementation of Consistent Color Generation.
//!
//! This is similar to Consistent Color Generation defined in XEP-0392,
//! but uses OKLCh colorspace instead of HSLuv
//! to ensure that colors have the same lightness.
use colorutils_rs::{Oklch, Rgb, TransferFunction};
//! Consistent Color Generation is defined in XEP-0392.
//!
//! Color Vision Deficiency correction is not implemented as Delta Chat does not offer
//! corresponding settings.
use hsluv::hsluv_to_rgb;
use sha1::{Digest, Sha1};
/// Converts an identifier to Hue angle.
fn str_to_angle(s: &str) -> f32 {
fn str_to_angle(s: &str) -> f64 {
let bytes = s.as_bytes();
let result = Sha1::digest(bytes);
let checksum: u16 = result.first().map_or(0, |&x| u16::from(x))
+ 256 * result.get(1).map_or(0, |&x| u16::from(x));
f32::from(checksum) / 65536.0 * 360.0
f64::from(checksum) / 65536.0 * 360.0
}
/// Converts RGB tuple to a 24-bit number.
///
/// Returns a 24-bit number with 8 least significant bits corresponding to the blue color and 8
/// most significant bits corresponding to the red color.
fn rgb_to_u32(rgb: Rgb<u8>) -> u32 {
65536 * u32::from(rgb.r) + 256 * u32::from(rgb.g) + u32::from(rgb.b)
fn rgb_to_u32((r, g, b): (f64, f64, f64)) -> u32 {
let r = ((r * 256.0) as u32).min(255);
let g = ((g * 256.0) as u32).min(255);
let b = ((b * 256.0) as u32).min(255);
65536 * r + 256 * g + b
}
/// Converts an identifier to RGB color.
///
/// Lightness is set to half (0.5) to make colors suitable both for light and dark theme.
/// Saturation is set to maximum (100.0) to make colors distinguishable, and lightness is set to
/// half (50.0) to make colors suitable both for light and dark theme.
pub fn str_to_color(s: &str) -> u32 {
let lightness = 0.5;
let chroma = 0.22;
let angle = str_to_angle(s);
let oklch = Oklch::new(lightness, chroma, angle);
let rgb = oklch.to_rgb(TransferFunction::Srgb);
rgb_to_u32(rgb)
rgb_to_u32(hsluv_to_rgb((str_to_angle(s), 100.0, 50.0)))
}
/// Returns color as a "#RRGGBB" `String` where R, G, B are hex digits.
@@ -46,7 +45,6 @@ mod tests {
use super::*;
#[test]
#[allow(clippy::excessive_precision)]
fn test_str_to_angle() {
// Test against test vectors from
// <https://xmpp.org/extensions/xep-0392.html#testvectors-fullrange-no-cvd>
@@ -59,11 +57,11 @@ mod tests {
#[test]
fn test_rgb_to_u32() {
assert_eq!(rgb_to_u32(Rgb::new(0, 0, 0)), 0);
assert_eq!(rgb_to_u32(Rgb::new(0xff, 0xff, 0xff)), 0xffffff);
assert_eq!(rgb_to_u32(Rgb::new(0, 0, 0xff)), 0x0000ff);
assert_eq!(rgb_to_u32(Rgb::new(0, 0xff, 0)), 0x00ff00);
assert_eq!(rgb_to_u32(Rgb::new(0xff, 0, 0)), 0xff0000);
assert_eq!(rgb_to_u32(Rgb::new(0xff, 0x80, 0)), 0xff8000);
assert_eq!(rgb_to_u32((0.0, 0.0, 0.0)), 0);
assert_eq!(rgb_to_u32((1.0, 1.0, 1.0)), 0xffffff);
assert_eq!(rgb_to_u32((0.0, 0.0, 1.0)), 0x0000ff);
assert_eq!(rgb_to_u32((0.0, 1.0, 0.0)), 0x00ff00);
assert_eq!(rgb_to_u32((1.0, 0.0, 0.0)), 0xff0000);
assert_eq!(rgb_to_u32((1.0, 0.5, 0.0)), 0xff8000);
}
}

View File

@@ -734,7 +734,7 @@ impl Context {
Self::check_config(key, value)?;
let _pause = match key.needs_io_restart() {
true => self.scheduler.pause(self).await?,
true => self.scheduler.pause(self.clone()).await?,
_ => Default::default(),
};
self.set_config_internal(key, value).await?;

View File

@@ -755,19 +755,7 @@ impl Contact {
self.is_bot
}
/// Looks up a known and unblocked contact with a given e-mail address.
/// To get a list of all known and unblocked contacts, use contacts_get_contacts().
///
///
/// **POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
/// (e.g. an address-contact and a key-contact),
/// this looks up the most recently seen contact,
/// i.e. which contact is returned depends on which contact last sent a message.
/// If the user just clicked on a mailto: link, then this is the best thing you can do.
/// But **DO NOT** internally represent contacts by their email address
/// and do not use this function to look them up;
/// otherwise this function will sometimes look up the wrong contact.
/// Instead, you should internally represent contacts by their ids.
/// Check if an e-mail address belongs to a known and unblocked contact.
///
/// Known and unblocked contacts will be returned by `get_contacts()`.
///
@@ -807,28 +795,14 @@ impl Contact {
.query_get_value(
"SELECT id FROM contacts
WHERE addr=?1 COLLATE NOCASE
AND id>?2 AND origin>=?3 AND (? OR blocked=?)
ORDER BY
(
SELECT COUNT(*) FROM chats c
INNER JOIN chats_contacts cc
ON c.id=cc.chat_id
WHERE c.type=?
AND c.id>?
AND c.blocked=?
AND cc.contact_id=contacts.id
) DESC,
last_seen DESC, fingerprint DESC
LIMIT 1",
AND fingerprint='' -- Do not lookup key-contacts
AND id>?2 AND origin>=?3 AND (? OR blocked=?)",
(
&addr_normalized,
ContactId::LAST_SPECIAL,
min_origin as u32,
blocked.is_none(),
blocked.unwrap_or(Blocked::Not),
Chattype::Single,
constants::DC_CHAT_ID_LAST_SPECIAL,
blocked.unwrap_or(Blocked::Not),
blocked.unwrap_or_default(),
),
)
.await?;
@@ -1564,7 +1538,7 @@ impl Contact {
return Ok(Some(chat::get_device_icon(context).await?));
}
if show_fallback_icon && !self.id.is_special() && !self.is_key_contact() {
return Ok(Some(chat::get_unencrypted_icon(context).await?));
return Ok(Some(chat::get_address_contact_icon(context).await?));
}
if let Some(image_rel) = self.param.get(Param::ProfileImage) {
if !image_rel.is_empty() {
@@ -1575,16 +1549,11 @@ impl Contact {
}
/// Get a color for the contact.
/// The color is calculated from the contact's fingerprint (for key-contacts)
/// or email address (for address-contacts) and can be used
/// for an fallback avatar with white initials
/// The color is calculated from the contact's email address
/// and can be used for an fallback avatar with white initials
/// as well as for headlines in bubbles of group chats.
pub fn get_color(&self) -> u32 {
if let Some(fingerprint) = self.fingerprint() {
str_to_color(&fingerprint.hex())
} else {
str_to_color(&self.addr.to_lowercase())
}
str_to_color(&self.addr.to_lowercase())
}
/// Gets the contact's status.
@@ -1982,9 +1951,8 @@ pub(crate) async fn mark_contact_id_as_verified(
}
}
transaction.execute(
"UPDATE contacts SET verifier=?1
WHERE id=?2 AND (verifier=0 OR ?1=?3)",
(verifier_id, contact_id, ContactId::SELF),
"UPDATE contacts SET verifier=? WHERE id=?",
(verifier_id, contact_id),
)?;
Ok(())
})

View File

@@ -759,7 +759,7 @@ async fn test_contact_get_color() -> Result<()> {
let t = TestContext::new().await;
let contact_id = Contact::create(&t, "name", "name@example.net").await?;
let color1 = Contact::get_by_id(&t, contact_id).await?.get_color();
assert_eq!(color1, 0x4947dc);
assert_eq!(color1, 0xA739FF);
let t = TestContext::new().await;
let contact_id = Contact::create(&t, "prename name", "name@example.net").await?;
@@ -1035,50 +1035,6 @@ async fn test_was_seen_recently_event() -> Result<()> {
Ok(())
}
async fn test_lookup_id_by_addr_recent_ex(accept_unencrypted_chat: bool) -> Result<()> {
let mut tcm = TestContextManager::new();
let bob = &tcm.bob().await;
let raw = include_bytes!("../../test-data/message/thunderbird_with_autocrypt.eml");
assert!(std::str::from_utf8(raw)?.contains("Date: Thu, 24 Nov 2022 20:05:57 +0100"));
let received_msg = receive_imf(bob, raw, false).await?.unwrap();
received_msg.chat_id.accept(bob).await?;
let raw = r#"From: Alice <alice@example.org>
To: bob@example.net
Message-ID: message$TIME@example.org
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Date: Thu, 24 Nov 2022 $TIME +0100
Hi"#
.to_string();
for (time, is_key_contact) in [("20:05:57", true), ("20:05:58", !accept_unencrypted_chat)] {
let raw = raw.replace("$TIME", time);
let received_msg = receive_imf(bob, raw.as_bytes(), false).await?.unwrap();
if accept_unencrypted_chat {
received_msg.chat_id.accept(bob).await?;
}
let contact_id = Contact::lookup_id_by_addr(bob, "alice@example.org", Origin::Unknown)
.await?
.unwrap();
let contact = Contact::get_by_id(bob, contact_id).await?;
assert_eq!(contact.is_key_contact(), is_key_contact);
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_lookup_id_by_addr_recent() -> Result<()> {
let accept_unencrypted_chat = true;
test_lookup_id_by_addr_recent_ex(accept_unencrypted_chat).await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_lookup_id_by_addr_recent_accepted() -> Result<()> {
let accept_unencrypted_chat = false;
test_lookup_id_by_addr_recent_ex(accept_unencrypted_chat).await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verified_by_none() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -1116,7 +1072,6 @@ async fn test_sync_create() -> Result<()> {
.unwrap();
let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?;
assert_eq!(a1b_contact.name, "Bob");
assert_eq!(a1b_contact.is_key_contact(), false);
Contact::create(alice0, "Bob Renamed", "bob@example.net").await?;
test_utils::sync(alice0, alice1).await;
@@ -1126,7 +1081,6 @@ async fn test_sync_create() -> Result<()> {
assert_eq!(id, a1b_contact_id);
let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?;
assert_eq!(a1b_contact.name, "Bob Renamed");
assert_eq!(a1b_contact.is_key_contact(), false);
Ok(())
}

View File

@@ -34,7 +34,7 @@ use crate::param::{Param, Params};
use crate::peer_channels::Iroh;
use crate::push::PushSubscriber;
use crate::quota::QuotaInfo;
use crate::scheduler::{ConnectivityStore, SchedulerState, convert_folder_meaning};
use crate::scheduler::{SchedulerState, convert_folder_meaning};
use crate::sql::Sql;
use crate::stock_str::StockStrings;
use crate::timesmearing::SmearedTimestamp;
@@ -304,10 +304,6 @@ pub struct InnerContext {
/// tokio::sync::OnceCell would be possible to use, but overkill for our usecase;
/// the standard library's OnceLock is enough, and it's a lot smaller in memory.
pub(crate) self_fingerprint: OnceLock<String>,
/// `Connectivity` values for mailboxes, unordered. Used to compute the aggregate connectivity,
/// see [`Context::get_connectivity()`].
pub(crate) connectivities: parking_lot::Mutex<Vec<ConnectivityStore>>,
}
/// The state of ongoing process.
@@ -477,7 +473,6 @@ impl Context {
push_subscribed: AtomicBool::new(false),
iroh: Arc::new(RwLock::new(None)),
self_fingerprint: OnceLock::new(),
connectivities: parking_lot::Mutex::new(Vec::new()),
};
let ctx = Context {
@@ -507,7 +502,7 @@ impl Context {
// Now, some configs may have changed, so, we need to invalidate the cache.
self.sql.config_cache.write().await.clear();
self.scheduler.start(self).await;
self.scheduler.start(self.clone()).await;
}
/// Stops the IO scheduler.
@@ -584,7 +579,7 @@ impl Context {
} else {
// Pause the scheduler to ensure another connection does not start
// while we are fetching on a dedicated connection.
let _pause_guard = self.scheduler.pause(self).await?;
let _pause_guard = self.scheduler.pause(self.clone()).await?;
// Start a new dedicated connection.
let mut connection = Imap::new_configured(self, channel::bounded(1).1).await?;
@@ -1092,6 +1087,7 @@ impl Context {
#[derive(Default)]
struct ChatNumbers {
protected: u32,
protection_broken: u32,
opportunistic_dc: u32,
opportunistic_mua: u32,
unencrypted_dc: u32,
@@ -1127,6 +1123,7 @@ impl Context {
// how many of the chats active in the last months are:
// - protected
// - protection-broken
// - opportunistic-encrypted and the contact uses Delta Chat
// - opportunistic-encrypted and the contact uses a classical MUA
// - unencrypted and the contact uses Delta Chat
@@ -1169,6 +1166,8 @@ impl Context {
if protected == ProtectionStatus::Protected {
chats.protected += 1;
} else if protected == ProtectionStatus::ProtectionBroken {
chats.protection_broken += 1;
} else if encrypted {
if is_dc_message {
chats.opportunistic_dc += 1;
@@ -1186,6 +1185,7 @@ impl Context {
)
.await?;
res += &format!("chats_protected {}\n", chats.protected);
res += &format!("chats_protection_broken {}\n", chats.protection_broken);
res += &format!("chats_opportunistic_dc {}\n", chats.opportunistic_dc);
res += &format!("chats_opportunistic_mua {}\n", chats.opportunistic_mua);
res += &format!("chats_unencrypted_dc {}\n", chats.unencrypted_dc);

View File

@@ -277,7 +277,6 @@ pub(crate) async fn stock_ephemeral_timer_changed(
.await
}
604_800 => stock_str::msg_ephemeral_timer_week(context, from_id).await,
31_536_000..=31_708_800 => stock_str::msg_ephemeral_timer_year(context, from_id).await,
_ => {
stock_str::msg_ephemeral_timer_weeks(
context,

View File

@@ -128,33 +128,31 @@ async fn test_stock_ephemeral_messages() {
/// Test enabling and disabling ephemeral timer remotely.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_ephemeral_enable_disable() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let chat_alice = alice.create_chat(bob).await.id;
let chat_bob = bob.create_chat(alice).await.id;
let chat_alice = alice.create_chat(&bob).await.id;
let chat_bob = bob.create_chat(&alice).await.id;
chat_alice
.set_ephemeral_timer(alice, Timer::Enabled { duration: 60 })
.set_ephemeral_timer(&alice.ctx, Timer::Enabled { duration: 60 })
.await?;
let sent = alice.pop_sent_msg().await;
let bob_received_message = bob.recv_msg(&sent).await;
bob.recv_msg(&sent).await;
assert_eq!(
bob_received_message.text,
"Message deletion timer is set to 1 minute by alice@example.org."
);
assert_eq!(
chat_bob.get_ephemeral_timer(bob).await?,
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
Timer::Enabled { duration: 60 }
);
chat_alice
.set_ephemeral_timer(alice, Timer::Disabled)
.set_ephemeral_timer(&alice.ctx, Timer::Disabled)
.await?;
let sent = alice.pop_sent_msg().await;
bob.recv_msg(&sent).await;
assert_eq!(chat_bob.get_ephemeral_timer(bob).await?, Timer::Disabled);
assert_eq!(
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
Timer::Disabled
);
Ok(())
}

View File

@@ -325,7 +325,7 @@ impl Imap {
}
info!(context, "Connecting to IMAP server.");
self.connectivity.set_connecting(context);
self.connectivity.set_connecting(context).await;
self.conn_last_try = tools::Time::now();
const BACKOFF_MIN_MS: u64 = 2000;
@@ -408,7 +408,7 @@ impl Imap {
"IMAP-LOGIN as {}",
lp.user
)));
self.connectivity.set_preparing(context);
self.connectivity.set_preparing(context).await;
info!(context, "Successfully logged into IMAP server.");
return Ok(session);
}
@@ -466,7 +466,7 @@ impl Imap {
let mut session = match self.connect(context, configuring).await {
Ok(session) => session,
Err(err) => {
self.connectivity.set_err(context, &err);
self.connectivity.set_err(context, &err).await;
return Err(err);
}
};
@@ -692,7 +692,7 @@ impl Imap {
}
if !uids_fetch.is_empty() {
self.connectivity.set_working(context);
self.connectivity.set_working(context).await;
}
let (sender, receiver) = async_channel::unbounded();

View File

@@ -90,7 +90,7 @@ pub async fn imex(
let cancel = context.alloc_ongoing().await?;
let res = {
let _guard = context.scheduler.pause(context).await?;
let _guard = context.scheduler.pause(context.clone()).await?;
imex_inner(context, what, path, passphrase)
.race(async {
cancel.recv().await.ok();

View File

@@ -105,7 +105,7 @@ impl BackupProvider {
// Acquire global "ongoing" mutex.
let cancel_token = context.alloc_ongoing().await?;
let paused_guard = context.scheduler.pause(context).await?;
let paused_guard = context.scheduler.pause(context.clone()).await?;
let context_dir = context
.get_blobdir()
.parent()
@@ -250,7 +250,7 @@ impl BackupProvider {
Err(format_err!("Backup provider dropped"))
}
).await {
error!(context, "Error while handling backup connection: {err:#}.");
warn!(context, "Error while handling backup connection: {err:#}.");
context.emit_event(EventType::ImexProgress(0));
break;
} else {
@@ -367,8 +367,7 @@ pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
Err(format_err!("Backup reception cancelled"))
})
.await;
if let Err(ref res) = res {
error!(context, "{:#}", res);
if res.is_err() {
context.emit_event(EventType::ImexProgress(0));
}
context.free_ongoing().await;

View File

@@ -1366,6 +1366,17 @@ impl Message {
Ok(())
}
pub(crate) async fn update_subject(&self, context: &Context) -> Result<()> {
context
.sql
.execute(
"UPDATE msgs SET subject=? WHERE id=?;",
(&self.subject, self.id),
)
.await?;
Ok(())
}
/// Gets the error status of the message.
///
/// A message can have an associated error status if something went wrong when sending or

View File

@@ -808,22 +808,3 @@ async fn test_sanitize_filename_message() -> Result<()> {
Ok(())
}
/// Tests that empty file can be sent and received.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_empty_file() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_chat = alice.create_chat(bob).await;
let mut msg = Message::new(Viewtype::File);
msg.set_file_from_bytes(alice, "myfile", b"", None)?;
chat::send_msg(alice, alice_chat.id, &mut msg).await?;
let sent = alice.pop_sent_msg().await;
let bob_received_msg = bob.recv_msg(&sent).await;
assert_eq!(bob_received_msg.get_filename().unwrap(), "myfile");
assert_eq!(bob_received_msg.get_viewtype(), Viewtype::File);
Ok(())
}

View File

@@ -91,11 +91,8 @@ fn test_render_rfc724_mid() {
fn render_header_text(text: &str) -> String {
let mut output = Vec::<u8>::new();
// Some non-zero length of the header name.
let bytes_written = 20;
mail_builder::headers::text::Text::new(text.to_string())
.write_header(&mut output, bytes_written)
.write_header(&mut output, 0)
.unwrap();
String::from_utf8(output).unwrap()
@@ -686,7 +683,6 @@ async fn test_selfavatar_unencrypted_signed() {
.unwrap()
.unwrap();
let alice_contact = Contact::get_by_id(&bob.ctx, alice_id).await.unwrap();
assert_eq!(alice_contact.is_key_contact(), false);
assert!(
alice_contact
.get_profile_image(&bob.ctx)

View File

@@ -427,7 +427,7 @@ impl MimeMessage {
None
};
let mut public_keyring = if incoming {
let public_keyring = if incoming {
if let Some(autocrypt_header) = autocrypt_header {
vec![autocrypt_header.public_key]
} else {
@@ -437,46 +437,8 @@ impl MimeMessage {
key::load_self_public_keyring(context).await?
};
if let Some(signature) = match &decrypted_msg {
Some(pgp::composed::Message::Literal { .. }) => None,
Some(pgp::composed::Message::Compressed { .. }) => {
// One layer of compression should already be handled by now.
// We don't decompress messages compressed multiple times.
None
}
Some(pgp::composed::Message::SignedOnePass { reader, .. }) => reader.signature(),
Some(pgp::composed::Message::Signed { reader, .. }) => Some(reader.signature()),
Some(pgp::composed::Message::Encrypted { .. }) => {
// The message is already decrypted once.
None
}
None => None,
} {
for issuer_fingerprint in signature.issuer_fingerprint() {
let issuer_fingerprint =
crate::key::Fingerprint::from(issuer_fingerprint.clone()).hex();
if let Some(public_key_bytes) = context
.sql
.query_row_optional(
"SELECT public_key
FROM public_keys
WHERE fingerprint=?",
(&issuer_fingerprint,),
|row| {
let bytes: Vec<u8> = row.get(0)?;
Ok(bytes)
},
)
.await?
{
let public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
public_keyring.push(public_key)
}
}
}
let mut signatures = if let Some(ref decrypted_msg) = decrypted_msg {
crate::pgp::valid_signature_fingerprints(decrypted_msg, &public_keyring)
crate::pgp::valid_signature_fingerprints(decrypted_msg, &public_keyring)?
} else {
HashSet::new()
};
@@ -1360,6 +1322,10 @@ impl MimeMessage {
filename: &str,
is_related: bool,
) -> Result<()> {
if decoded_data.is_empty() {
return Ok(());
}
// Process attached PGP keys.
if mime_type.type_() == mime::APPLICATION
&& mime_type.subtype().as_str() == "pgp-keys"

View File

@@ -227,6 +227,9 @@ pub(crate) async fn update_connect_timestamp(
}
/// Preloaded DNS results that can be used in case of DNS server failures.
///
/// See <https://support.delta.chat/t/no-dns-resolution-result/2778> and
/// <https://github.com/deltachat/deltachat-core-rust/issues/4920> for reasons.
static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new(|| {
HashMap::from([
(

View File

@@ -244,7 +244,7 @@ async fn fetch_url(context: &Context, original_url: &str) -> Result<Response> {
.clone();
let req = hyper::Request::builder()
.uri(parsed_url)
.uri(parsed_url.path())
.header(hyper::header::HOST, authority.as_str())
.body(http_body_util::Empty::<Bytes>::new())?;
let response = sender.send_request(req).await?;
@@ -378,7 +378,7 @@ pub(crate) async fn post_string(context: &Context, url: &str, body: String) -> R
.context("URL has no authority")?
.clone();
let request = hyper::Request::post(parsed_url)
let request = hyper::Request::post(parsed_url.path())
.header(hyper::header::HOST, authority.as_str())
.body(body)?;
let response = sender.send_request(request).await?;
@@ -408,7 +408,7 @@ pub(crate) async fn post_form<T: Serialize + ?Sized>(
.authority()
.context("URL has no authority")?
.clone();
let request = hyper::Request::post(parsed_url)
let request = hyper::Request::post(parsed_url.path())
.header(hyper::header::HOST, authority.as_str())
.header("content-type", "application/x-www-form-urlencoded")
.body(encoded_body)?;

View File

@@ -272,7 +272,7 @@ pub fn pk_decrypt(
pub fn valid_signature_fingerprints(
msg: &pgp::composed::Message,
public_keys_for_validation: &[SignedPublicKey],
) -> HashSet<Fingerprint> {
) -> Result<HashSet<Fingerprint>> {
let mut ret_signature_fingerprints: HashSet<Fingerprint> = Default::default();
if msg.is_signed() {
for pkey in public_keys_for_validation {
@@ -282,7 +282,7 @@ pub fn valid_signature_fingerprints(
}
}
}
ret_signature_fingerprints
Ok(ret_signature_fingerprints)
}
/// Validates detached signature.
@@ -359,7 +359,7 @@ mod tests {
let mut msg = pk_decrypt(ctext.to_vec(), private_keys_for_decryption)?;
let content = msg.as_data_vec()?;
let ret_signature_fingerprints =
valid_signature_fingerprints(&msg, public_keys_for_validation);
valid_signature_fingerprints(&msg, public_keys_for_validation)?;
Ok((msg, ret_signature_fingerprints, content))
}

View File

@@ -404,7 +404,7 @@ mod tests {
use crate::config::Config;
use crate::contact::{Contact, Origin};
use crate::download::DownloadState;
use crate::message::{MessageState, Viewtype, delete_msgs};
use crate::message::{MessageState, delete_msgs};
use crate::receive_imf::{receive_imf, receive_imf_from_inbox};
use crate::sql::housekeeping;
use crate::test_utils::E2EE_INFO_MSGS;
@@ -550,46 +550,6 @@ Here's my footer -- bob@example.net"
let reactions = get_msg_reactions(&alice, msg.id).await?;
assert_eq!(reactions.to_string(), "😀1");
// Alice receives a message with reaction to her message from Bob.
let msg_bob = receive_imf(
&alice,
"To: alice@example.org\n\
From: bob@example.net\n\
Date: Today, 29 February 2021 00:00:10 -800\n\
Message-ID: 56791@example.net\n\
In-Reply-To: 12345@example.org\n\
Mime-Version: 1.0\n\
Content-Type: multipart/mixed; boundary=\"YiEDa0DAkWCtVeE4\"\n\
Content-Disposition: inline\n\
\n\
--YiEDa0DAkWCtVeE4\n\
Content-Type: text/plain; charset=utf-8\n\
Content-Disposition: inline\n\
\n\
Reply + reaction\n\
\n\
--YiEDa0DAkWCtVeE4\n\
Content-Type: text/plain; charset=utf-8\n\
Content-Disposition: reaction\n\
\n\
\u{1F44D}\n\
\n\
--YiEDa0DAkWCtVeE4--"
.as_bytes(),
false,
)
.await?
.unwrap();
let msg_bob = Message::load_from_db(&alice, msg_bob.msg_ids[0]).await?;
assert_eq!(msg_bob.from_id, bob_id);
assert_eq!(msg_bob.chat_id, msg.chat_id);
assert_eq!(msg_bob.viewtype, Viewtype::Text);
assert_eq!(msg_bob.state, MessageState::InFresh);
assert_eq!(msg_bob.hidden, false);
assert_eq!(msg_bob.text, "Reply + reaction");
let reactions = get_msg_reactions(&alice, msg.id).await?;
assert_eq!(reactions.to_string(), "👍1");
Ok(())
}

View File

@@ -18,7 +18,7 @@ use crate::chat::{
self, Chat, ChatId, ChatIdBlocked, ProtectionStatus, remove_from_chat_contacts_table,
};
use crate::config::Config;
use crate::constants::{self, Blocked, Chattype, DC_CHAT_ID_TRASH, EDITED_PREFIX, ShowEmails};
use crate::constants::{Blocked, Chattype, DC_CHAT_ID_TRASH, EDITED_PREFIX, ShowEmails};
use crate::contact::{Contact, ContactId, Origin, mark_contact_id_as_verified};
use crate::context::Context;
use crate::debug_logging::maybe_set_logging_xdc_inner;
@@ -297,16 +297,18 @@ async fn get_to_and_past_contact_ids(
past_member_fingerprints = &[];
}
let pgp_to_ids = add_or_lookup_key_contacts_by_address_list(
context,
&mime_parser.recipients,
&mime_parser.gossiped_keys,
to_member_fingerprints,
Origin::Hidden,
)
.await?;
match chat_assignment {
ChatAssignment::GroupChat { .. } => {
to_ids = add_or_lookup_key_contacts(
context,
&mime_parser.recipients,
&mime_parser.gossiped_keys,
to_member_fingerprints,
Origin::Hidden,
)
.await?;
to_ids = pgp_to_ids;
if let Some(chat_id) = chat_id {
past_ids = lookup_key_contacts_by_address_list(
@@ -317,7 +319,7 @@ async fn get_to_and_past_contact_ids(
)
.await?;
} else {
past_ids = add_or_lookup_key_contacts(
past_ids = add_or_lookup_key_contacts_by_address_list(
context,
&mime_parser.past_members,
&mime_parser.gossiped_keys,
@@ -334,14 +336,7 @@ async fn get_to_and_past_contact_ids(
ChatAssignment::ExistingChat { chat_id, .. } => {
let chat = Chat::load_from_db(context, *chat_id).await?;
if chat.is_encrypted(context).await? {
to_ids = add_or_lookup_key_contacts(
context,
&mime_parser.recipients,
&mime_parser.gossiped_keys,
to_member_fingerprints,
Origin::Hidden,
)
.await?;
to_ids = pgp_to_ids;
past_ids = lookup_key_contacts_by_address_list(
context,
&mime_parser.past_members,
@@ -393,14 +388,6 @@ async fn get_to_and_past_contact_ids(
.await?;
}
ChatAssignment::OneOneChat => {
let pgp_to_ids = add_or_lookup_key_contacts(
context,
&mime_parser.recipients,
&mime_parser.gossiped_keys,
to_member_fingerprints,
Origin::Hidden,
)
.await?;
if pgp_to_ids
.first()
.is_some_and(|contact_id| contact_id.is_some())
@@ -442,7 +429,7 @@ async fn get_to_and_past_contact_ids(
context,
&mime_parser.recipients,
to_member_fingerprints,
None,
chat_id,
)
.await?
}
@@ -630,7 +617,8 @@ pub(crate) async fn receive_imf_inner(
return Ok(None);
};
let prevent_rename = should_prevent_rename(&mime_parser);
let prevent_rename = (mime_parser.is_mailinglist_message() && !mime_parser.was_encrypted())
|| mime_parser.get_header(HeaderDef::Sender).is_some();
// get From: (it can be an address list!) and check if it is known (for known From:'s we add
// the other To:/Cc: in the 3rd pass)
@@ -763,6 +751,7 @@ pub(crate) async fn receive_imf_inner(
let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?)
.unwrap_or_default();
let is_reaction = mime_parser.parts.iter().any(|part| part.is_reaction);
let allow_creation = if mime_parser.decrypting_failed {
false
} else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage
@@ -776,7 +765,7 @@ pub(crate) async fn receive_imf_inner(
ShowEmails::All => true,
}
} else {
!mime_parser.parts.iter().all(|part| part.is_reaction)
!is_reaction
};
let to_id = if mime_parser.incoming {
@@ -1217,21 +1206,17 @@ async fn decide_chat_assignment(
//
// The chat may not exist yet, i.e. there may be
// no database row and ChatId yet.
let mut num_recipients = 0;
let mut has_self_addr = false;
for recipient in &mime_parser.recipients {
if addr_cmp(&recipient.addr, &mime_parser.from.addr) {
continue;
let mut num_recipients = mime_parser.recipients.len();
if from_id != ContactId::SELF {
let mut has_self_addr = false;
for recipient in &mime_parser.recipients {
if context.is_self_addr(&recipient.addr).await? {
has_self_addr = true;
}
}
if context.is_self_addr(&recipient.addr).await? {
has_self_addr = true;
if !has_self_addr {
num_recipients += 1;
}
num_recipients += 1;
}
if from_id != ContactId::SELF && !has_self_addr {
num_recipients += 1;
}
let chat_assignment = if should_trash {
@@ -1274,15 +1259,11 @@ async fn decide_chat_assignment(
chat_id,
chat_id_blocked,
}
} else if mime_parser.get_header(HeaderDef::ChatGroupName).is_some() {
ChatAssignment::AdHocGroup
} else if num_recipients <= 1 {
ChatAssignment::OneOneChat
} else {
ChatAssignment::AdHocGroup
}
} else if mime_parser.get_header(HeaderDef::ChatGroupName).is_some() {
ChatAssignment::AdHocGroup
} else if num_recipients <= 1 {
ChatAssignment::OneOneChat
} else {
@@ -1403,6 +1384,7 @@ async fn do_chat_assignment(
context,
mime_parser,
to_ids,
from_id,
allow_creation || test_normal_chat.is_some(),
create_blocked,
is_partial_download.is_some(),
@@ -1479,16 +1461,19 @@ async fn do_chat_assignment(
chat.typ == Chattype::Single,
"Chat {chat_id} is not Single",
);
let new_protection = match verified_encryption {
let mut new_protection = match verified_encryption {
VerifiedEncryption::Verified => ProtectionStatus::Protected,
VerifiedEncryption::NotVerified(_) => ProtectionStatus::Unprotected,
};
ensure_and_debug_assert!(
chat.protected == ProtectionStatus::Unprotected
|| new_protection == ProtectionStatus::Protected,
"Chat {chat_id} can't downgrade to Unprotected",
);
if chat.protected != ProtectionStatus::Unprotected
&& new_protection == ProtectionStatus::Unprotected
// `chat.protected` must be maintained regardless of the `Config::VerifiedOneOnOneChats`.
// That's why the config is checked here, and not above.
&& context.get_config_bool(Config::VerifiedOneOnOneChats).await?
{
new_protection = ProtectionStatus::ProtectionBroken;
}
if chat.protected != new_protection {
// The message itself will be sorted under the device message since the device
// message is `MessageState::InNoticed`, which means that all following
@@ -1572,6 +1557,7 @@ async fn do_chat_assignment(
context,
mime_parser,
to_ids,
from_id,
allow_creation,
Blocked::Not,
is_partial_download.is_some(),
@@ -1676,12 +1662,12 @@ async fn add_parts(
}
}
let mut chat = Chat::load_from_db(context, chat_id).await?;
if mime_parser.incoming && !chat_id.is_trash() {
// It can happen that the message is put into a chat
// but the From-address is not a member of this chat.
if !chat::is_contact_in_chat(context, chat_id, from_id).await? {
let chat = Chat::load_from_db(context, chat_id).await?;
// Mark the sender as overridden.
// The UI will prepend `~` to the sender's name,
// indicating that the sender is not part of the group.
@@ -1702,6 +1688,7 @@ async fn add_parts(
let is_location_kml = mime_parser.location_kml.is_some();
let is_mdn = !mime_parser.mdn_reports.is_empty();
let mut chat = Chat::load_from_db(context, chat_id).await?;
let mut group_changes = match chat.typ {
_ if chat.id.is_special() => GroupChangesInfo::default(),
Chattype::Single => GroupChangesInfo::default(),
@@ -1851,8 +1838,6 @@ async fn add_parts(
{
Some(stock_str::msg_location_enabled_by(context, from_id).await)
} else if mime_parser.is_system_message == SystemMessage::EphemeralTimerChanged {
let better_msg = stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await;
// Do not delete the system message itself.
//
// This prevents confusion when timer is changed
@@ -1861,13 +1846,15 @@ async fn add_parts(
// week is left.
ephemeral_timer = EphemeralTimer::Disabled;
Some(better_msg)
Some(stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await)
} else {
None
};
let mut verification_failed = false;
// if a chat is protected and the message is fully downloaded, check additional properties
if !chat_id.is_special() && is_partial_download.is_none() {
let chat = Chat::load_from_db(context, chat_id).await?;
// For outgoing emails in the 1:1 chat we have an exception that
// they are allowed to be unencrypted:
// 1. They can't be an attack (they are outgoing, not incoming)
@@ -1878,14 +1865,12 @@ async fn add_parts(
// likely reinstalled DC" or similar) would be wrong.
if chat.is_protected() && (mime_parser.incoming || chat.typ != Chattype::Single) {
if let VerifiedEncryption::NotVerified(err) = verified_encryption {
verification_failed = true;
warn!(context, "Verification problem: {err:#}.");
let s = format!("{err}. Re-download the message or see 'Info' for more details");
let s = format!("{err}. See 'Info' for more details");
mime_parser.replace_msg_by_error(&s);
}
}
}
drop(chat); // Avoid using stale `chat` object.
let sort_timestamp = tweak_sort_timestamp(
context,
@@ -1998,10 +1983,10 @@ async fn add_parts(
handle_edit_delete(context, mime_parser, from_id).await?;
let hidden = mime_parser.parts.iter().all(|part| part.is_reaction);
let is_reaction = mime_parser.parts.iter().any(|part| part.is_reaction);
let hidden = is_reaction;
let mut parts = mime_parser.parts.iter().peekable();
while let Some(part) = parts.next() {
let hidden = part.is_reaction;
if part.is_reaction {
let reaction_str = simplify::remove_footers(part.msg.as_str());
let is_incoming_fresh = mime_parser.incoming && !seen;
@@ -2142,10 +2127,6 @@ RETURNING id
DownloadState::Available
} else if mime_parser.decrypting_failed {
DownloadState::Undecipherable
} else if verification_failed {
// Verification can fail because of message reordering. Re-downloading the
// message should help if so.
DownloadState::Available
} else {
DownloadState::Done
},
@@ -2471,6 +2452,7 @@ async fn lookup_or_create_adhoc_group(
context: &Context,
mime_parser: &MimeMessage,
to_ids: &[Option<ContactId>],
from_id: ContactId,
allow_creation: bool,
create_blocked: Blocked,
is_partial_download: bool,
@@ -2493,29 +2475,10 @@ async fn lookup_or_create_adhoc_group(
return Ok(None);
}
// Lookup address-contact by the From address.
let fingerprint = None;
let find_key_contact_by_addr = false;
let prevent_rename = should_prevent_rename(mime_parser);
let (from_id, _from_id_blocked, _incoming_origin) = from_field_to_contact_id(
context,
&mime_parser.from,
fingerprint,
prevent_rename,
find_key_contact_by_addr,
)
.await?
.context("Cannot lookup address-contact by the From field")?;
let grpname = mime_parser
.get_header(HeaderDef::ChatGroupName)
.map(|s| s.to_string())
.unwrap_or_else(|| {
mime_parser
.get_subject()
.map(|s| remove_subject_prefix(&s))
.unwrap_or_else(|| "👥📧".to_string())
});
.get_subject()
.map(|s| remove_subject_prefix(&s))
.unwrap_or_else(|| "👥📧".to_string());
let to_ids: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
let mut contact_ids = Vec::with_capacity(to_ids.len() + 1);
contact_ids.extend(&to_ids);
@@ -2880,13 +2843,20 @@ async fn apply_group_changes(
let is_from_in_chat =
!chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
if mime_parser.get_header(HeaderDef::ChatVerified).is_some() && !chat.is_protected() {
if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
if let VerifiedEncryption::NotVerified(err) = verified_encryption {
warn!(
context,
"Not marking chat {} as protected due to verification problem: {err:#}.", chat.id,
);
} else {
if chat.is_protected() {
warn!(context, "Verification problem: {err:#}.");
let s = format!("{err}. See 'Info' for more details");
mime_parser.replace_msg_by_error(&s);
} else {
warn!(
context,
"Not marking chat {} as protected due to verification problem: {err:#}.",
chat.id
);
}
} else if !chat.is_protected() {
chat.id
.set_protection(
context,
@@ -3760,7 +3730,7 @@ async fn add_or_lookup_contacts_by_address_list(
}
/// Looks up contact IDs from the database given the list of recipients.
async fn add_or_lookup_key_contacts(
async fn add_or_lookup_key_contacts_by_address_list(
context: &Context,
address_list: &[SingleInfo],
gossiped_keys: &HashMap<String, SignedPublicKey>,
@@ -3780,9 +3750,6 @@ async fn add_or_lookup_key_contacts(
fp.hex()
} else if let Some(key) = gossiped_keys.get(addr) {
key.dc_fingerprint().hex()
} else if context.is_self_addr(addr).await? {
contact_ids.push(Some(ContactId::SELF));
continue;
} else {
contact_ids.push(None);
continue;
@@ -3811,16 +3778,13 @@ async fn add_or_lookup_key_contacts(
/// Looks up a key-contact by email address.
///
/// If provided, `chat_id` must be an encrypted chat ID that has key-contacts inside.
/// Otherwise the function searches in all contacts, preferring accepted and most recently seen ones.
/// Otherwise the function searches in all contacts, returning the recently seen one.
async fn lookup_key_contact_by_address(
context: &Context,
addr: &str,
chat_id: Option<ChatId>,
) -> Result<Option<ContactId>> {
if context.is_self_addr(addr).await? {
if chat_id.is_none() {
return Ok(Some(ContactId::SELF));
}
let is_self_in_chat = context
.sql
.exists(
@@ -3857,26 +3821,11 @@ async fn lookup_key_contact_by_address(
.sql
.query_row_optional(
"SELECT id FROM contacts
WHERE addr=?
WHERE contacts.addr=?1
AND fingerprint<>''
ORDER BY
(
SELECT COUNT(*) FROM chats c
INNER JOIN chats_contacts cc
ON c.id=cc.chat_id
WHERE c.type=?
AND c.id>?
AND c.blocked=?
AND cc.contact_id=contacts.id
) DESC,
last_seen DESC, id DESC
ORDER BY last_seen DESC, id DESC
",
(
addr,
Chattype::Single,
constants::DC_CHAT_ID_LAST_SPECIAL,
Blocked::Not,
),
(addr,),
|row| {
let contact_id: ContactId = row.get(0)?;
Ok(contact_id)
@@ -3984,12 +3933,5 @@ async fn lookup_key_contacts_by_address_list(
Ok(contact_ids)
}
/// Returns true if the message should not result in renaming
/// of the sender contact.
fn should_prevent_rename(mime_parser: &MimeMessage) -> bool {
(mime_parser.is_mailinglist_message() && !mime_parser.was_encrypted())
|| mime_parser.get_header(HeaderDef::Sender).is_some()
}
#[cfg(test)]
mod receive_imf_tests;

View File

@@ -991,7 +991,6 @@ async fn test_other_device_writes_to_mailinglist() -> Result<()> {
.await?
.unwrap();
let list_post_contact = Contact::get_by_id(&t, list_post_contact_id).await?;
assert_eq!(list_post_contact.is_key_contact(), false);
assert_eq!(
list_post_contact.param.get(Param::ListId).unwrap(),
"delta.codespeak.net"
@@ -1454,7 +1453,6 @@ async fn test_apply_mailinglist_changes_assigned_by_reply() {
.unwrap()
.unwrap();
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.is_key_contact(), false);
assert_eq!(
contact.param.get(Param::ListId).unwrap(),
"deltachat-core-rust.deltachat.github.com"
@@ -3316,31 +3314,6 @@ async fn test_thunderbird_autocrypt() -> Result<()> {
Ok(())
}
/// Tests that a message without an Autocrypt header is assigned to the key-contact
/// by using the signature Issuer Fingerprint.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_issuer_fingerprint() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_contact_id = bob.add_or_lookup_contact_id(alice).await;
let raw = include_bytes!("../../test-data/message/encrypted-signed.eml");
let received_msg = receive_imf(bob, raw, false).await?.unwrap();
assert_eq!(received_msg.msg_ids.len(), 1);
let msg_id = received_msg.msg_ids[0];
let message = Message::load_from_db(bob, msg_id).await?;
assert!(message.get_showpadlock());
let from_id = message.from_id;
assert_eq!(from_id, alice_contact_id);
Ok(())
}
/// Tests reception of a message from Thunderbird with attached key.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_prefer_encrypt_mutual_if_encrypted() -> Result<()> {
@@ -3709,13 +3682,10 @@ async fn test_mua_user_adds_recipient_to_single_chat() -> Result<()> {
chat::get_chat_contacts(&alice, group_chat.id).await?.len(),
4
);
let fiona_contact_id =
Contact::lookup_id_by_addr(&alice, "fiona@example.net", Origin::IncomingTo)
.await?
.unwrap();
assert!(chat::is_contact_in_chat(&alice, group_chat.id, fiona_contact_id).await?);
let fiona_contact = Contact::get_by_id(&alice, fiona_contact_id).await?;
assert_eq!(fiona_contact.is_key_contact(), false);
let fiona = Contact::lookup_id_by_addr(&alice, "fiona@example.net", Origin::IncomingTo)
.await?
.unwrap();
assert!(chat::is_contact_in_chat(&alice, group_chat.id, fiona).await?);
Ok(())
}
@@ -5116,44 +5086,6 @@ async fn test_two_group_securejoins() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_unverified_member_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
let alice_chat_id =
chat::create_group_chat(alice, ProtectionStatus::Protected, "Group").await?;
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await?;
tcm.exec_securejoin_qr(bob, alice, &qr).await;
tcm.exec_securejoin_qr(fiona, alice, &qr).await;
let fiona_chat_id = fiona.get_last_msg().await.chat_id;
let fiona_sent_msg = fiona.send_text(fiona_chat_id, "Hi").await;
// The message can't be verified, but the user can re-download it.
let bob_msg = bob.recv_msg(&fiona_sent_msg).await;
assert_eq!(bob_msg.download_state, DownloadState::Available);
assert!(
bob_msg
.text
.contains("Re-download the message or see 'Info' for more details")
);
let alice_sent_msg = alice
.send_text(alice_chat_id, "Hi all, it's Alice introducing Fiona")
.await;
bob.recv_msg(&alice_sent_msg).await;
// Now Bob has Fiona's key and can verify the message.
let bob_msg = bob.recv_msg(&fiona_sent_msg).await;
assert_eq!(bob_msg.download_state, DownloadState::Done);
assert_eq!(bob_msg.text, "Hi");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sanitize_filename_in_received() -> Result<()> {
let alice = &TestContext::new_alice().await;
@@ -5370,176 +5302,3 @@ async fn test_outgoing_unencrypted_chat_assignment() {
let chat = alice.create_email_chat(bob).await;
assert_eq!(received.chat_id, chat.id);
}
/// Tests Bob receiving a message from Alice
/// in a new group she just created
/// with only Alice and Bob.
///
/// The message has no Autocrypt-Gossip
/// headers and no Chat-Group-Member-Fpr header.
/// Such messages were created by core 1.159.5
/// when Alice has bcc_self disabled
/// as Chat-Group-Member-Fpr header did not exist
/// yet and Autocrypt-Gossip is not sent
/// as there is only one recipient
/// (Bob, and no additional Alice devices).
///
/// Bob should recognize self as being
/// a member of the group by just the e-mail address.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_group_introduction_no_gossip() -> Result<()> {
let mut tcm = TestContextManager::new();
let bob = &tcm.bob().await;
let received = receive_imf(
bob,
include_bytes!("../../test-data/message/group-introduction-no-gossip.eml"),
false,
)
.await
.unwrap()
.unwrap();
let msg = Message::load_from_db(bob, *received.msg_ids.last().unwrap())
.await
.unwrap();
assert_eq!(msg.text, "I created a group");
let chat = Chat::load_from_db(bob, msg.chat_id).await.unwrap();
assert_eq!(chat.typ, Chattype::Group);
assert_eq!(chat.blocked, Blocked::Request);
assert_eq!(chat.name, "Group!");
assert!(chat.is_encrypted(bob).await.unwrap());
let contacts = get_chat_contacts(bob, chat.id).await?;
assert_eq!(contacts.len(), 2);
assert!(chat.is_self_in_chat(bob).await?);
Ok(())
}
/// Tests reception of an encrypted group message
/// without Chat-Group-ID.
///
/// The message should be displayed as
/// encrypted and have key-contact `from_id`,
/// but all group members should be address-contacts.
///
/// Due to a bug in v2.10.0 this resulted
/// in creation of an ad hoc group with a key-contact.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_encrypted_adhoc_group_message() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
// Bob receives encrypted message from Alice
// sent to multiple recipients,
// but without a group ID.
let received = receive_imf(
bob,
include_bytes!("../../test-data/message/encrypted-group-without-id.eml"),
false,
)
.await?
.unwrap();
let msg = Message::load_from_db(bob, *received.msg_ids.last().unwrap())
.await
.unwrap();
let chat = Chat::load_from_db(bob, msg.chat_id).await.unwrap();
assert_eq!(chat.typ, Chattype::Group);
assert_eq!(chat.is_encrypted(bob).await?, false);
let contact_ids = get_chat_contacts(bob, chat.id).await?;
assert_eq!(contact_ids.len(), 3);
assert!(chat.is_self_in_chat(bob).await?);
// Since the group is unencrypted, all contacts have
// to be address-contacts.
for contact_id in contact_ids {
let contact = Contact::get_by_id(bob, contact_id).await?;
if contact_id != ContactId::SELF {
assert_eq!(contact.is_key_contact(), false);
}
}
// `from_id` of the message corresponds to key-contact of Alice
// even though the message is assigned to unencrypted chat.
let alice_contact_id = bob.add_or_lookup_contact_id(alice).await;
assert_eq!(msg.from_id, alice_contact_id);
Ok(())
}
/// Tests that messages sent to unencrypted group
/// with only two members arrive in a group
/// and not in 1:1 chat.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_small_unencrypted_group() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_chat_id = chat::create_group_ex(alice, None, "Unencrypted group").await?;
let alice_bob_id = alice.add_or_lookup_address_contact_id(bob).await;
add_contact_to_chat(alice, alice_chat_id, alice_bob_id).await?;
send_text_msg(alice, alice_chat_id, "Hello!".to_string()).await?;
let sent_msg = alice.pop_sent_msg().await;
let bob_chat_id = bob.recv_msg(&sent_msg).await.chat_id;
let bob_chat = Chat::load_from_db(bob, bob_chat_id).await?;
assert_eq!(bob_chat.typ, Chattype::Group);
assert_eq!(bob_chat.is_encrypted(bob).await?, false);
bob_chat_id.accept(bob).await?;
send_text_msg(bob, bob_chat_id, "Hello back!".to_string()).await?;
let sent_msg = bob.pop_sent_msg().await;
let alice_rcvd_msg = alice.recv_msg(&sent_msg).await;
assert_eq!(alice_rcvd_msg.chat_id, alice_chat_id);
Ok(())
}
/// Tests that if the sender includes self
/// in the `To` field, we do not count
/// it as a third recipient in addition to ourselves
/// and the sender and do not create a group chat.
///
/// This is a regression test.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_bcc_not_a_group() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let received = receive_imf(
alice,
b"From: \"\"<foobar@example.org>\n\
To: <foobar@example.org>\n\
Subject: Hello, this is not a group\n\
Message-ID: <abcdef@example.org>\n\
Chat-Version: 1.0\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello\n",
false,
)
.await?
.unwrap();
let received_chat = Chat::load_from_db(alice, received.chat_id).await?;
assert_eq!(received_chat.typ, Chattype::Single);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_lookup_key_contact_by_address_self() -> Result<()> {
let mut tcm = TestContextManager::new();
let t = &tcm.alice().await;
let addr = &t.get_config(Config::Addr).await?.unwrap();
assert_eq!(
lookup_key_contact_by_address(t, addr, None).await?,
Some(ContactId::SELF)
);
Ok(())
}

View File

@@ -8,12 +8,12 @@ use async_channel::{self as channel, Receiver, Sender};
use futures::future::try_join_all;
use futures_lite::FutureExt;
use rand::Rng;
use tokio::sync::{RwLock, oneshot};
use tokio::sync::{RwLock, RwLockWriteGuard, oneshot};
use tokio::task;
use tokio_util::sync::CancellationToken;
use tokio_util::task::TaskTracker;
pub(crate) use self::connectivity::ConnectivityStore;
use self::connectivity::ConnectivityStore;
use crate::config::{self, Config};
use crate::constants;
use crate::contact::{ContactId, RecentlySeenLoop};
@@ -53,32 +53,32 @@ impl SchedulerState {
}
/// Starts the scheduler if it is not yet started.
pub(crate) async fn start(&self, context: &Context) {
pub(crate) async fn start(&self, context: Context) {
let mut inner = self.inner.write().await;
match *inner {
InnerSchedulerState::Started(_) => (),
InnerSchedulerState::Stopped => Self::do_start(&mut inner, context).await,
InnerSchedulerState::Stopped => Self::do_start(inner, context).await,
InnerSchedulerState::Paused {
ref mut started, ..
} => *started = true,
}
context.update_connectivities(&inner);
}
/// Starts the scheduler if it is not yet started.
async fn do_start(inner: &mut InnerSchedulerState, context: &Context) {
async fn do_start(mut inner: RwLockWriteGuard<'_, InnerSchedulerState>, context: Context) {
info!(context, "starting IO");
// Notify message processing loop
// to allow processing old messages after restart.
context.new_msgs_notify.notify_one();
match Scheduler::start(context).await {
let ctx = context.clone();
match Scheduler::start(&context).await {
Ok(scheduler) => {
*inner = InnerSchedulerState::Started(scheduler);
context.emit_event(EventType::ConnectivityChanged);
}
Err(err) => error!(context, "Failed to start IO: {:#}", err),
Err(err) => error!(&ctx, "Failed to start IO: {:#}", err),
}
}
@@ -87,19 +87,18 @@ impl SchedulerState {
let mut inner = self.inner.write().await;
match *inner {
InnerSchedulerState::Started(_) => {
Self::do_stop(&mut inner, context, InnerSchedulerState::Stopped).await
Self::do_stop(inner, context, InnerSchedulerState::Stopped).await
}
InnerSchedulerState::Stopped => (),
InnerSchedulerState::Paused {
ref mut started, ..
} => *started = false,
}
context.update_connectivities(&inner);
}
/// Stops the scheduler if it is currently running.
async fn do_stop(
inner: &mut InnerSchedulerState,
mut inner: RwLockWriteGuard<'_, InnerSchedulerState>,
context: &Context,
new_state: InnerSchedulerState,
) {
@@ -123,7 +122,7 @@ impl SchedulerState {
debug_logging.loop_handle.abort();
debug_logging.loop_handle.await.ok();
}
let prev_state = std::mem::replace(inner, new_state);
let prev_state = std::mem::replace(&mut *inner, new_state);
context.emit_event(EventType::ConnectivityChanged);
match prev_state {
InnerSchedulerState::Started(scheduler) => scheduler.stop(context).await,
@@ -139,7 +138,7 @@ impl SchedulerState {
/// If in the meantime [`SchedulerState::start`] or [`SchedulerState::stop`] is called
/// resume will do the right thing and restore the scheduler to the state requested by
/// the last call.
pub(crate) async fn pause(&'_ self, context: &Context) -> Result<IoPausedGuard> {
pub(crate) async fn pause(&'_ self, context: Context) -> Result<IoPausedGuard> {
{
let mut inner = self.inner.write().await;
match *inner {
@@ -148,7 +147,7 @@ impl SchedulerState {
started: true,
pause_guards_count: NonZeroUsize::new(1).unwrap(),
};
Self::do_stop(&mut inner, context, new_state).await;
Self::do_stop(inner, &context, new_state).await;
}
InnerSchedulerState::Stopped => {
*inner = InnerSchedulerState::Paused {
@@ -165,11 +164,9 @@ impl SchedulerState {
.ok_or_else(|| Error::msg("Too many pause guards active"))?
}
}
context.update_connectivities(&inner);
}
let (tx, rx) = oneshot::channel();
let context = context.clone();
tokio::spawn(async move {
rx.await.ok();
let mut inner = context.scheduler.inner.write().await;
@@ -186,7 +183,7 @@ impl SchedulerState {
} => {
if *pause_guards_count == NonZeroUsize::new(1).unwrap() {
match *started {
true => SchedulerState::do_start(&mut inner, &context).await,
true => SchedulerState::do_start(inner, context.clone()).await,
false => *inner = InnerSchedulerState::Stopped,
}
} else {
@@ -196,7 +193,6 @@ impl SchedulerState {
}
}
}
context.update_connectivities(&inner);
});
Ok(IoPausedGuard { sender: Some(tx) })
}
@@ -206,7 +202,7 @@ impl SchedulerState {
info!(context, "restarting IO");
if self.is_running().await {
self.stop(context).await;
self.start(context).await;
self.start(context.clone()).await;
}
}
@@ -227,7 +223,7 @@ impl SchedulerState {
_ => return,
};
drop(inner);
connectivity::idle_interrupted(inbox, oboxes);
connectivity::idle_interrupted(inbox, oboxes).await;
}
/// Indicate that the network likely is lost.
@@ -244,7 +240,7 @@ impl SchedulerState {
_ => return,
};
drop(inner);
connectivity::maybe_network_lost(context, stores);
connectivity::maybe_network_lost(context, stores).await;
}
pub(crate) async fn interrupt_inbox(&self) {
@@ -292,7 +288,7 @@ impl SchedulerState {
}
#[derive(Debug, Default)]
pub(crate) enum InnerSchedulerState {
enum InnerSchedulerState {
Started(Scheduler),
#[default]
Stopped,
@@ -569,7 +565,7 @@ async fn fetch_idle(
// The folder is not configured.
// For example, this happens if the server does not have Sent folder
// but watching Sent folder is enabled.
connection.connectivity.set_not_configured(ctx);
connection.connectivity.set_not_configured(ctx).await;
connection.idle_interrupt_receiver.recv().await.ok();
bail!("Cannot fetch folder {folder_meaning} because it is not configured");
};
@@ -659,7 +655,7 @@ async fn fetch_idle(
.log_err(ctx)
.ok();
connection.connectivity.set_idle(ctx);
connection.connectivity.set_idle(ctx).await;
ctx.emit_event(EventType::ImapInboxIdle);
@@ -810,8 +806,8 @@ async fn smtp_loop(
// Fake Idle
info!(ctx, "SMTP fake idle started.");
match &connection.last_send_error {
None => connection.connectivity.set_idle(&ctx),
Some(err) => connection.connectivity.set_err(&ctx, err),
None => connection.connectivity.set_idle(&ctx).await,
Some(err) => connection.connectivity.set_err(&ctx, err).await,
}
// If send_smtp_messages() failed, we set a timeout for the fake-idle so that

View File

@@ -4,6 +4,7 @@ use std::{iter::once, ops::Deref, sync::Arc};
use anyhow::Result;
use humansize::{BINARY, format_size};
use tokio::sync::Mutex;
use crate::events::EventType;
use crate::imap::{FolderMeaning, scan_folders::get_watched_folder_configs};
@@ -159,51 +160,52 @@ impl DetailedConnectivity {
}
#[derive(Clone, Default)]
pub(crate) struct ConnectivityStore(Arc<parking_lot::Mutex<DetailedConnectivity>>);
pub(crate) struct ConnectivityStore(Arc<Mutex<DetailedConnectivity>>);
impl ConnectivityStore {
fn set(&self, context: &Context, v: DetailedConnectivity) {
async fn set(&self, context: &Context, v: DetailedConnectivity) {
{
*self.0.lock() = v;
*self.0.lock().await = v;
}
context.emit_event(EventType::ConnectivityChanged);
}
pub(crate) fn set_err(&self, context: &Context, e: impl ToString) {
self.set(context, DetailedConnectivity::Error(e.to_string()));
pub(crate) async fn set_err(&self, context: &Context, e: impl ToString) {
self.set(context, DetailedConnectivity::Error(e.to_string()))
.await;
}
pub(crate) fn set_connecting(&self, context: &Context) {
self.set(context, DetailedConnectivity::Connecting);
pub(crate) async fn set_connecting(&self, context: &Context) {
self.set(context, DetailedConnectivity::Connecting).await;
}
pub(crate) fn set_working(&self, context: &Context) {
self.set(context, DetailedConnectivity::Working);
pub(crate) async fn set_working(&self, context: &Context) {
self.set(context, DetailedConnectivity::Working).await;
}
pub(crate) fn set_preparing(&self, context: &Context) {
self.set(context, DetailedConnectivity::Preparing);
pub(crate) async fn set_preparing(&self, context: &Context) {
self.set(context, DetailedConnectivity::Preparing).await;
}
pub(crate) fn set_not_configured(&self, context: &Context) {
self.set(context, DetailedConnectivity::NotConfigured);
pub(crate) async fn set_not_configured(&self, context: &Context) {
self.set(context, DetailedConnectivity::NotConfigured).await;
}
pub(crate) fn set_idle(&self, context: &Context) {
self.set(context, DetailedConnectivity::Idle);
pub(crate) async fn set_idle(&self, context: &Context) {
self.set(context, DetailedConnectivity::Idle).await;
}
fn get_detailed(&self) -> DetailedConnectivity {
self.0.lock().deref().clone()
async fn get_detailed(&self) -> DetailedConnectivity {
self.0.lock().await.deref().clone()
}
fn get_basic(&self) -> Option<Connectivity> {
self.0.lock().to_basic()
async fn get_basic(&self) -> Option<Connectivity> {
self.0.lock().await.to_basic()
}
fn get_all_work_done(&self) -> bool {
self.0.lock().all_work_done()
async fn get_all_work_done(&self) -> bool {
self.0.lock().await.all_work_done()
}
}
/// Set all folder states to InterruptingIdle in case they were `Idle` before.
/// Called during `dc_maybe_network()` to make sure that `all_work_done()`
/// returns false immediately after `dc_maybe_network()`.
pub(crate) fn idle_interrupted(inbox: ConnectivityStore, oboxes: Vec<ConnectivityStore>) {
let mut connectivity_lock = inbox.0.lock();
pub(crate) async fn idle_interrupted(inbox: ConnectivityStore, oboxes: Vec<ConnectivityStore>) {
let mut connectivity_lock = inbox.0.lock().await;
// For the inbox, we also have to set the connectivity to InterruptingIdle if it was
// NotConfigured before: If all folders are NotConfigured, dc_get_connectivity()
// returns Connected. But after dc_maybe_network(), dc_get_connectivity() must not
@@ -217,7 +219,7 @@ pub(crate) fn idle_interrupted(inbox: ConnectivityStore, oboxes: Vec<Connectivit
drop(connectivity_lock);
for state in oboxes {
let mut connectivity_lock = state.0.lock();
let mut connectivity_lock = state.0.lock().await;
if *connectivity_lock == DetailedConnectivity::Idle {
*connectivity_lock = DetailedConnectivity::InterruptingIdle;
}
@@ -229,9 +231,9 @@ pub(crate) fn idle_interrupted(inbox: ConnectivityStore, oboxes: Vec<Connectivit
/// Set the connectivity to "Not connected" after a call to dc_maybe_network_lost().
/// If we did not do this, the connectivity would stay "Connected" for quite a long time
/// after `maybe_network_lost()` was called.
pub(crate) fn maybe_network_lost(context: &Context, stores: Vec<ConnectivityStore>) {
pub(crate) async fn maybe_network_lost(context: &Context, stores: Vec<ConnectivityStore>) {
for store in &stores {
let mut connectivity_lock = store.0.lock();
let mut connectivity_lock = store.0.lock().await;
if !matches!(
*connectivity_lock,
DetailedConnectivity::Uninitialized
@@ -246,7 +248,7 @@ pub(crate) fn maybe_network_lost(context: &Context, stores: Vec<ConnectivityStor
impl fmt::Debug for ConnectivityStore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(guard) = self.0.try_lock() {
if let Ok(guard) = self.0.try_lock() {
write!(f, "ConnectivityStore {:?}", &*guard)
} else {
write!(f, "ConnectivityStore [LOCKED]")
@@ -269,29 +271,27 @@ impl Context {
/// e.g. in the title of the main screen.
///
/// If the connectivity changes, a DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
pub fn get_connectivity(&self) -> Connectivity {
let stores = self.connectivities.lock().clone();
pub async fn get_connectivity(&self) -> Connectivity {
let lock = self.scheduler.inner.read().await;
let stores: Vec<_> = match *lock {
InnerSchedulerState::Started(ref sched) => sched
.boxes()
.map(|b| b.conn_state.state.connectivity.clone())
.collect(),
_ => return Connectivity::NotConnected,
};
drop(lock);
let mut connectivities = Vec::new();
for s in stores {
if let Some(connectivity) = s.get_basic() {
if let Some(connectivity) = s.get_basic().await {
connectivities.push(connectivity);
}
}
connectivities
.into_iter()
.min()
.unwrap_or(Connectivity::NotConnected)
}
pub(crate) fn update_connectivities(&self, sched: &InnerSchedulerState) {
let stores: Vec<_> = match sched {
InnerSchedulerState::Started(sched) => sched
.boxes()
.map(|b| b.conn_state.state.connectivity.clone())
.collect(),
_ => Vec::new(),
};
*self.connectivities.lock() = stores;
.unwrap_or(Connectivity::Connected)
}
/// Get an overview of the current connectivity, and possibly more statistics.
@@ -391,7 +391,7 @@ impl Context {
let f = self.get_config(config).await.log_err(self).ok().flatten();
if let Some(foldername) = f {
let detailed = &state.get_detailed();
let detailed = &state.get_detailed().await;
ret += "<li>";
ret += &*detailed.to_icon();
ret += " <b>";
@@ -405,7 +405,7 @@ impl Context {
}
if !folder_added && folder == &FolderMeaning::Inbox {
let detailed = &state.get_detailed();
let detailed = &state.get_detailed().await;
if let DetailedConnectivity::Error(_) = detailed {
// On the inbox thread, we also do some other things like scan_folders and run jobs
// so, maybe, the inbox is not watched, but something else went wrong
@@ -427,7 +427,7 @@ impl Context {
let outgoing_messages = stock_str::outgoing_messages(self).await;
ret += &format!("<h3>{outgoing_messages}</h3><ul><li>");
let detailed = smtp.get_detailed();
let detailed = smtp.get_detailed().await;
ret += &*detailed.to_icon();
ret += " ";
ret += &*escaper::encode_minimal(&detailed.to_string_smtp(self).await);
@@ -551,7 +551,7 @@ impl Context {
drop(lock);
for s in &stores {
if !s.get_all_work_done() {
if !s.get_all_work_done().await {
return false;
}
}

View File

@@ -1,6 +1,6 @@
//! Implementation of [SecureJoin protocols](https://securejoin.delta.chat/).
use anyhow::{Context as _, Error, Result, bail, ensure};
use anyhow::{Context as _, Error, Result, ensure};
use deltachat_contact_tools::ContactAddress;
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
@@ -63,11 +63,10 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
chat.typ == Chattype::Group,
"Can't generate SecureJoin QR code for 1:1 chat {id}"
);
if chat.grpid.is_empty() {
let err = format!("Can't generate QR code, chat {id} is a email thread");
error!(context, "get_securejoin_qr: {}.", err);
bail!(err);
}
ensure!(
!chat.grpid.is_empty(),
"Can't generate SecureJoin QR code for ad-hoc group {id}"
);
Some(chat)
}
None => None,

View File

@@ -87,7 +87,7 @@ impl Smtp {
return Ok(());
}
self.connectivity.set_connecting(context);
self.connectivity.set_connecting(context).await;
let lp = ConfiguredLoginParam::load(context)
.await?
.context("Not configured")?;
@@ -187,7 +187,7 @@ pub(crate) async fn smtp_send(
info!(context, "SMTP-sending out mime message:\n{message}");
}
smtp.connectivity.set_working(context);
smtp.connectivity.set_working(context).await;
if let Err(err) = smtp
.connect_configured(context)
@@ -414,11 +414,7 @@ pub(crate) async fn send_msg_to_smtp(
.await?;
}
SendResult::Failure(ref err) => {
if err
.to_string()
.to_lowercase()
.contains("invalid unencrypted mail")
{
if err.to_string().contains("Invalid unencrypted mail") {
let res = context
.sql
.query_row_optional(

View File

@@ -3,7 +3,7 @@
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use anyhow::{Context as _, Result, bail, ensure};
use anyhow::{Context as _, Result, bail};
use rusqlite::{Connection, OpenFlags, Row, config::DbConfig, types::ValueRef};
use tokio::sync::RwLock;
@@ -23,7 +23,7 @@ use crate::net::http::http_cache_cleanup;
use crate::net::prune_connection_history;
use crate::param::{Param, Params};
use crate::stock_str;
use crate::tools::{SystemTime, Time, delete_file, time, time_elapsed};
use crate::tools::{SystemTime, delete_file, time};
/// Extension to [`rusqlite::ToSql`] trait
/// which also includes [`Send`] and [`Sync`].
@@ -175,12 +175,10 @@ impl Sql {
.await
}
const N_DB_CONNECTIONS: usize = 3;
/// Creates a new connection pool.
fn new_pool(dbfile: &Path, passphrase: String) -> Result<Pool> {
let mut connections = Vec::with_capacity(Self::N_DB_CONNECTIONS);
for _ in 0..Self::N_DB_CONNECTIONS {
let mut connections = Vec::new();
for _ in 0..3 {
let connection = new_connection(dbfile, &passphrase)?;
connections.push(connection);
}
@@ -639,77 +637,6 @@ impl Sql {
pub fn config_cache(&self) -> &RwLock<HashMap<String, Option<String>>> {
&self.config_cache
}
/// Runs a checkpoint operation in TRUNCATE mode, so the WAL file is truncated to 0 bytes.
pub(crate) async fn wal_checkpoint(context: &Context) -> Result<()> {
let t_start = Time::now();
let lock = context.sql.pool.read().await;
let Some(pool) = lock.as_ref() else {
// No db connections, nothing to checkpoint.
return Ok(());
};
// Do as much work as possible without blocking anybody.
let query_only = true;
let conn = pool.get(query_only).await?;
tokio::task::block_in_place(|| {
// Execute some transaction causing the WAL file to be opened so that the
// `wal_checkpoint()` can proceed, otherwise it fails when called the first time,
// see https://sqlite.org/forum/forumpost/7512d76a05268fc8.
conn.query_row("PRAGMA table_list", [], |_| Ok(()))?;
conn.query_row("PRAGMA wal_checkpoint(PASSIVE)", [], |_| Ok(()))
})?;
// Kick out writers.
const _: () = assert!(Sql::N_DB_CONNECTIONS > 1, "Deadlock possible");
let _write_lock = pool.write_lock().await;
let t_writers_blocked = Time::now();
// Ensure that all readers use the most recent database snapshot (are at the end of WAL) so
// that `wal_checkpoint(FULL)` isn't blocked. We could use `PASSIVE` as well, but it's
// documented poorly, https://www.sqlite.org/pragma.html#pragma_wal_checkpoint and
// https://www.sqlite.org/c3ref/wal_checkpoint_v2.html don't tell how it interacts with new
// readers.
let mut read_conns = Vec::with_capacity(Self::N_DB_CONNECTIONS - 1);
for _ in 0..(Self::N_DB_CONNECTIONS - 1) {
read_conns.push(pool.get(query_only).await?);
}
read_conns.clear();
// Checkpoint the remaining WAL pages without blocking readers.
let (pages_total, pages_checkpointed) = tokio::task::block_in_place(|| {
conn.query_row("PRAGMA wal_checkpoint(FULL)", [], |row| {
let pages_total: i64 = row.get(1)?;
let pages_checkpointed: i64 = row.get(2)?;
Ok((pages_total, pages_checkpointed))
})
})?;
if pages_checkpointed < pages_total {
warn!(
context,
"Cannot checkpoint whole WAL. Pages total: {pages_total}, checkpointed: {pages_checkpointed}. Make sure there are no external connections running transactions.",
);
}
// Kick out readers to avoid blocking/SQLITE_BUSY.
for _ in 0..(Self::N_DB_CONNECTIONS - 1) {
read_conns.push(pool.get(query_only).await?);
}
let t_readers_blocked = Time::now();
tokio::task::block_in_place(|| {
let blocked = conn.query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |row| {
let blocked: i64 = row.get(0)?;
Ok(blocked)
})?;
ensure!(blocked == 0);
Ok(())
})?;
info!(
context,
"wal_checkpoint: Total time: {:?}. Writers blocked for: {:?}. Readers blocked for: {:?}.",
time_elapsed(&t_start),
time_elapsed(&t_writers_blocked),
time_elapsed(&t_readers_blocked),
);
Ok(())
}
}
/// Creates a new SQLite connection.
@@ -833,14 +760,6 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
if let Err(err) = incremental_vacuum(context).await {
warn!(context, "Failed to run incremental vacuum: {err:#}.");
}
// Work around possible checkpoint starvations (there were cases reported when a WAL file is
// bigger than 200M) and also make sure we truncate the WAL periodically. Auto-checkponting does
// not normally truncate the WAL (unless the `journal_size_limit` pragma is set), see
// https://www.sqlite.org/wal.html.
if let Err(err) = Sql::wal_checkpoint(context).await {
warn!(context, "wal_checkpoint() failed: {err:#}.");
debug_assert!(false);
}
context
.sql

View File

@@ -1251,16 +1251,6 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint);
.await?;
}
inc_and_check(&mut migration_version, 133)?;
if dbversion < migration_version {
// Make `ProtectionBroken` chats `Unprotected`. Chats can't break anymore.
sql.execute_migration(
"UPDATE chats SET protected=0 WHERE protected!=1",
migration_version,
)
.await?;
}
let new_version = sql
.get_raw_config_int(VERSION_CFG)
.await?
@@ -1490,11 +1480,9 @@ fn migrate_key_contacts(
} else if addr.is_empty() {
Ok(default)
} else {
Ok(original_contact_id_from_addr_stmt
original_contact_id_from_addr_stmt
.query_row((addr,), |row| row.get(0))
.optional()
.with_context(|| format!("Original contact '{addr}' not found"))?
.unwrap_or(default))
.with_context(|| format!("Original contact '{addr}' not found"))
}
};

View File

@@ -2,7 +2,6 @@ use super::*;
use crate::chat;
use crate::chat::ChatId;
use crate::config::Config;
use crate::constants;
use crate::contact::Contact;
use crate::contact::ContactId;
use crate::contact::Origin;
@@ -44,11 +43,18 @@ async fn test_key_contacts_migration_autocrypt() -> Result<()> {
t.sql.run_migrations(&t).await?;
//std::thread::sleep(std::time::Duration::from_secs(1000));
let pgp_bob_id = Contact::lookup_id_by_addr(&t, "bob@example.net", Origin::Hidden)
let email_bob_id = Contact::lookup_id_by_addr(&t, "bob@example.net", Origin::Hidden)
.await?
.unwrap();
let email_bob = Contact::get_by_id(&t, email_bob_id).await?;
assert_eq!(email_bob.origin, Origin::Hidden); // Email bob is in no chats, so, contact is hidden
assert_eq!(email_bob.e2ee_avail(&t).await?, false);
assert_eq!(email_bob.fingerprint(), None);
assert_eq!(email_bob.get_verifier_id(&t).await?, None);
let bob_chat_contacts = chat::get_chat_contacts(&t, ChatId::new(10)).await?;
let pgp_bob_id = tools::single_value(bob_chat_contacts).unwrap();
let pgp_bob = Contact::get_by_id(&t, pgp_bob_id).await?;
assert_eq!(pgp_bob.is_key_contact(), true);
assert_eq!(pgp_bob.origin, Origin::OutgoingTo);
assert_eq!(pgp_bob.e2ee_avail(&t).await?, true);
assert_eq!(
@@ -57,16 +63,6 @@ async fn test_key_contacts_migration_autocrypt() -> Result<()> {
);
assert_eq!(pgp_bob.get_verifier_id(&t).await?, None);
// Hidden address-contact can't be looked up.
assert!(
Contact::get_all(&t, constants::DC_GCL_ADDRESS, Some("bob@example.net"))
.await?
.is_empty()
);
let bob_chat_contacts = chat::get_chat_contacts(&t, ChatId::new(10)).await?;
assert_eq!(tools::single_value(bob_chat_contacts).unwrap(), pgp_bob_id);
Ok(())
}
@@ -85,12 +81,10 @@ async fn test_key_contacts_migration_email1() -> Result<()> {
)?)).await?;
t.sql.run_migrations(&t).await?;
let email_bob_id = *Contact::get_all(&t, constants::DC_GCL_ADDRESS, Some("bob@example.net"))
let email_bob_id = Contact::lookup_id_by_addr(&t, "bob@example.net", Origin::Hidden)
.await?
.first()
.unwrap();
let email_bob = Contact::get_by_id(&t, email_bob_id).await?;
assert_eq!(email_bob.is_key_contact(), false);
assert_eq!(email_bob.origin, Origin::OutgoingTo);
assert_eq!(email_bob.e2ee_avail(&t).await?, false);
assert_eq!(email_bob.fingerprint(), None);
@@ -116,23 +110,11 @@ async fn test_key_contacts_migration_email2() -> Result<()> {
)?)).await?;
t.sql.run_migrations(&t).await?;
// Hidden key-contact can't be looked up.
assert!(
Contact::get_all(&t, 0, Some("bob@example.net"))
.await?
.is_empty()
);
let pgp_bob = Contact::get_by_id(&t, ContactId::new(11)).await?;
assert_eq!(pgp_bob.is_key_contact(), true);
assert_eq!(pgp_bob.origin, Origin::Hidden);
let email_bob_id = *Contact::get_all(&t, constants::DC_GCL_ADDRESS, Some("bob@example.net"))
let email_bob_id = Contact::lookup_id_by_addr(&t, "bob@example.net", Origin::Hidden)
.await?
.first()
.unwrap();
let email_bob = Contact::get_by_id(&t, email_bob_id).await?;
assert_eq!(email_bob.is_key_contact(), false);
assert_eq!(email_bob.origin, Origin::OutgoingTo);
assert_eq!(email_bob.origin, Origin::OutgoingTo); // Email bob is in no chats, so, contact is hidden
assert_eq!(email_bob.e2ee_avail(&t).await?, false);
assert_eq!(email_bob.fingerprint(), None);
assert_eq!(email_bob.get_verifier_id(&t).await?, None);
@@ -161,12 +143,15 @@ async fn test_key_contacts_migration_verified() -> Result<()> {
)?)).await?;
t.sql.run_migrations(&t).await?;
// Hidden address-contact can't be looked up.
assert!(
Contact::get_all(&t, constants::DC_GCL_ADDRESS, Some("bob@example.net"))
.await?
.is_empty()
);
let email_bob_id = Contact::lookup_id_by_addr(&t, "bob@example.net", Origin::Hidden)
.await?
.unwrap();
let email_bob = Contact::get_by_id(&t, email_bob_id).await?;
dbg!(&email_bob);
assert_eq!(email_bob.origin, Origin::Hidden); // Email bob is in no chats, so, contact is hidden
assert_eq!(email_bob.e2ee_avail(&t).await?, false);
assert_eq!(email_bob.fingerprint(), None);
assert_eq!(email_bob.get_verifier_id(&t).await?, None);
let mut bob_chat_contacts = chat::get_chat_contacts(&t, ChatId::new(10)).await?;
assert_eq!(bob_chat_contacts.len(), 2);

View File

@@ -67,7 +67,7 @@ struct InnerPool {
///
/// This mutex is locked when write connection
/// is outside the pool.
pub(crate) write_mutex: Arc<Mutex<()>>,
write_mutex: Arc<Mutex<()>>,
}
impl InnerPool {
@@ -96,13 +96,13 @@ impl InnerPool {
.pop()
.context("Got a permit when there are no connections in the pool")?
};
conn.pragma_update(None, "query_only", "1")?;
let conn = PooledConnection {
pool: Arc::downgrade(&self),
conn: Some(conn),
_permit: permit,
_write_mutex_guard: None,
};
conn.pragma_update(None, "query_only", "1")?;
Ok(conn)
} else {
// We get write guard first to avoid taking a permit
@@ -119,13 +119,13 @@ impl InnerPool {
"Got a permit and write lock when there are no connections in the pool",
)?
};
conn.pragma_update(None, "query_only", "0")?;
let conn = PooledConnection {
pool: Arc::downgrade(&self),
conn: Some(conn),
_permit: permit,
_write_mutex_guard: Some(write_mutex_guard),
};
conn.pragma_update(None, "query_only", "0")?;
Ok(conn)
}
}
@@ -195,12 +195,4 @@ impl Pool {
pub async fn get(&self, query_only: bool) -> Result<PooledConnection> {
Arc::clone(&self.inner).get(query_only).await
}
/// Returns a mutex guard guaranteeing that there are no concurrent write connections.
///
/// NB: Make sure you're not holding all connections when calling this, otherwise it deadlocks
/// if there is a concurrent writer waiting for available connection.
pub(crate) async fn write_lock(&self) -> OwnedMutexGuard<()> {
Arc::clone(&self.inner.write_mutex).lock_owned().await
}
}

View File

@@ -362,12 +362,6 @@ pub enum StockMessage {
#[strum(props(fallback = "Message deletion timer is set to %1$s weeks by %2$s."))]
MsgEphemeralTimerWeeksBy = 157,
#[strum(props(fallback = "You set message deletion timer to 1 year."))]
MsgYouEphemeralTimerYear = 158,
#[strum(props(fallback = "Message deletion timer is set to 1 year by %1$s."))]
MsgEphemeralTimerYearBy = 159,
#[strum(props(fallback = "Scan to set up second device for %1$s"))]
BackupTransferQr = 162,
@@ -998,17 +992,6 @@ pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: Cont
}
}
/// Stock string: `Message deletion timer is set to 1 year.`.
pub(crate) async fn msg_ephemeral_timer_year(context: &Context, by_contact: ContactId) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouEphemeralTimerYear).await
} else {
translated(context, StockMessage::MsgEphemeralTimerYearBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
}
}
/// Stock string: `Video chat invitation`.
pub(crate) async fn videochat_invitation(context: &Context) -> String {
translated(context, StockMessage::VideochatInvitation).await
@@ -1309,7 +1292,7 @@ impl Context {
contact_id: Option<ContactId>,
) -> String {
match protect {
ProtectionStatus::Unprotected => {
ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {
if let Some(contact_id) = contact_id {
chat_protection_disabled(self, contact_id).await
} else {

View File

@@ -1,7 +1,7 @@
//! Utilities to help writing tests.
//!
//! This private module is only compiled for test runs.
use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::collections::{BTreeMap, HashSet};
use std::env::current_dir;
use std::fmt::Write;
use std::ops::{Deref, DerefMut};
@@ -70,22 +70,19 @@ static CONTEXT_NAMES: LazyLock<std::sync::RwLock<BTreeMap<u32, String>>> =
/// [`TestContext`]s without managing your own [`LogSink`].
pub struct TestContextManager {
log_sink: LogSink,
used_names: BTreeSet<String>,
}
impl TestContextManager {
pub fn new() -> Self {
Self {
log_sink: LogSink::new(),
used_names: BTreeSet::new(),
}
let log_sink = LogSink::new();
Self { log_sink }
}
pub async fn alice(&mut self) -> TestContext {
TestContext::builder()
.configure_alice()
.with_log_sink(self.log_sink.clone())
.build(Some(&mut self.used_names))
.build()
.await
}
@@ -93,7 +90,7 @@ impl TestContextManager {
TestContext::builder()
.configure_bob()
.with_log_sink(self.log_sink.clone())
.build(Some(&mut self.used_names))
.build()
.await
}
@@ -101,7 +98,7 @@ impl TestContextManager {
TestContext::builder()
.configure_charlie()
.with_log_sink(self.log_sink.clone())
.build(Some(&mut self.used_names))
.build()
.await
}
@@ -109,7 +106,7 @@ impl TestContextManager {
TestContext::builder()
.configure_dom()
.with_log_sink(self.log_sink.clone())
.build(Some(&mut self.used_names))
.build()
.await
}
@@ -117,7 +114,7 @@ impl TestContextManager {
TestContext::builder()
.configure_elena()
.with_log_sink(self.log_sink.clone())
.build(Some(&mut self.used_names))
.build()
.await
}
@@ -125,7 +122,7 @@ impl TestContextManager {
TestContext::builder()
.configure_fiona()
.with_log_sink(self.log_sink.clone())
.build(Some(&mut self.used_names))
.build()
.await
}
@@ -133,7 +130,7 @@ impl TestContextManager {
pub async fn unconfigured(&mut self) -> TestContext {
TestContext::builder()
.with_log_sink(self.log_sink.clone())
.build(Some(&mut self.used_names))
.build()
.await
}
@@ -329,7 +326,7 @@ impl TestContextBuilder {
}
/// Builds the [`TestContext`].
pub async fn build(self, used_names: Option<&mut BTreeSet<String>>) -> TestContext {
pub async fn build(self) -> TestContext {
if let Some(key_pair) = self.key_pair {
let userid = {
let public_key = &key_pair.public;
@@ -343,19 +340,7 @@ impl TestContextBuilder {
.addr;
let name = EmailAddress::new(&addr).unwrap().local;
let mut unused_name = name.clone();
if let Some(used_names) = used_names {
assert!(used_names.len() < 100);
// If there are multiple Alices, call them 'alice', 'alice2', 'alice3', ...
let mut i = 1;
while used_names.contains(&unused_name) {
i += 1;
unused_name = format!("{name}{i}");
}
used_names.insert(unused_name.clone());
}
let test_context = TestContext::new_internal(Some(unused_name), self.log_sink).await;
let test_context = TestContext::new_internal(Some(name), self.log_sink).await;
test_context.configure_addr(&addr).await;
key::store_self_keypair(&test_context, &key_pair)
.await
@@ -409,21 +394,21 @@ impl TestContext {
///
/// This is a shortcut which configures alice@example.org with a fixed key.
pub async fn new_alice() -> Self {
Self::builder().configure_alice().build(None).await
Self::builder().configure_alice().build().await
}
/// Creates a new configured [`TestContext`].
///
/// This is a shortcut which configures bob@example.net with a fixed key.
pub async fn new_bob() -> Self {
Self::builder().configure_bob().build(None).await
Self::builder().configure_bob().build().await
}
/// Creates a new configured [`TestContext`].
///
/// This is a shortcut which configures fiona@example.net with a fixed key.
pub async fn new_fiona() -> Self {
Self::builder().configure_fiona().build(None).await
Self::builder().configure_fiona().build().await
}
/// Print current chat state.
@@ -1600,14 +1585,14 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_with_alice() {
let alice = TestContext::builder().configure_alice().build(None).await;
let alice = TestContext::builder().configure_alice().build().await;
alice.ctx.emit_event(EventType::Info("hello".into()));
// panic!("Alice fails");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_with_bob() {
let bob = TestContext::builder().configure_bob().build(None).await;
let bob = TestContext::builder().configure_bob().build().await;
bob.ctx.emit_event(EventType::Info("there".into()));
// panic!("Bob fails");
}

View File

@@ -31,7 +31,7 @@ async fn test_verified_oneonone_chat_not_broken_by_device_change() {
check_verified_oneonone_chat_protection_not_broken(false).await;
}
async fn check_verified_oneonone_chat_protection_not_broken(by_classical_email: bool) {
async fn check_verified_oneonone_chat_protection_not_broken(broken_by_classical_email: bool) {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
@@ -42,7 +42,7 @@ async fn check_verified_oneonone_chat_protection_not_broken(by_classical_email:
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
assert_verified(&bob, &alice, ProtectionStatus::Protected).await;
if by_classical_email {
if broken_by_classical_email {
tcm.section("Bob uses a classical MUA to send a message to Alice");
receive_imf(
&alice,
@@ -58,6 +58,7 @@ async fn check_verified_oneonone_chat_protection_not_broken(by_classical_email:
.await
.unwrap()
.unwrap();
// Bob's contact is still verified, but the chat isn't marked as protected anymore
let contact = alice.add_or_lookup_contact(&bob).await;
assert_eq!(contact.is_verified(&alice).await.unwrap(), true);
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
@@ -198,6 +199,7 @@ async fn test_missing_key_reexecute_securejoin() -> Result<()> {
let chat_id = tcm.execute_securejoin(bob, alice).await;
let chat = Chat::load_from_db(bob, chat_id).await?;
assert!(chat.is_protected());
assert!(!chat.is_protection_broken());
Ok(())
}
@@ -211,6 +213,7 @@ async fn test_create_unverified_oneonone_chat() -> Result<()> {
// A chat with an unknown contact should be created unprotected
let chat = alice.create_chat(&bob).await;
assert!(!chat.is_protected());
assert!(!chat.is_protection_broken());
receive_imf(
&alice,
@@ -227,12 +230,14 @@ async fn test_create_unverified_oneonone_chat() -> Result<()> {
// Now Bob is a known contact, new chats should still be created unprotected
let chat = alice.create_chat(&bob).await;
assert!(!chat.is_protected());
assert!(!chat.is_protection_broken());
tcm.send_recv(&bob, &alice, "hi").await;
chat.id.delete(&alice).await.unwrap();
// Now we have a public key, new chats should still be created unprotected
let chat = alice.create_chat(&bob).await;
assert!(!chat.is_protected());
assert!(!chat.is_protection_broken());
Ok(())
}
@@ -520,6 +525,7 @@ async fn test_message_from_old_dc_setup() -> Result<()> {
assert!(contact.is_verified(alice).await.unwrap());
let chat = alice.get_chat(bob).await;
assert!(chat.is_protected());
assert_eq!(chat.is_protection_broken(), false);
Ok(())
}
@@ -803,85 +809,22 @@ async fn test_verified_chat_editor_reordering() -> Result<()> {
Ok(())
}
/// Tests that already verified contact
/// does not get a new "verifier"
/// via gossip.
///
/// Directly verifying is still possible.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_no_reverification() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let charlie = &tcm.charlie().await;
let fiona = &tcm.fiona().await;
tcm.execute_securejoin(alice, bob).await;
tcm.execute_securejoin(alice, charlie).await;
tcm.execute_securejoin(alice, fiona).await;
tcm.section("Alice creates a protected group with Bob, Charlie and Fiona");
let alice_chat_id = alice
.create_group_with_members(ProtectionStatus::Protected, "Group", &[bob, charlie, fiona])
.await;
let alice_sent = alice.send_text(alice_chat_id, "Hi!").await;
let bob_rcvd_msg = bob.recv_msg(&alice_sent).await;
let bob_alice_id = bob_rcvd_msg.from_id;
// Charlie is verified by Alice for Bob.
let bob_charlie_contact = bob.add_or_lookup_contact(charlie).await;
assert_eq!(
bob_charlie_contact
.get_verifier_id(bob)
.await?
.unwrap()
.unwrap(),
bob_alice_id
);
let fiona_rcvd_msg = fiona.recv_msg(&alice_sent).await;
let fiona_chat_id = fiona_rcvd_msg.chat_id;
let fiona_sent = fiona.send_text(fiona_chat_id, "Post by Fiona").await;
bob.recv_msg(&fiona_sent).await;
// Charlie should still be verified by Alice, not by Fiona.
let bob_charlie_contact = bob.add_or_lookup_contact(charlie).await;
assert_eq!(
bob_charlie_contact
.get_verifier_id(bob)
.await?
.unwrap()
.unwrap(),
bob_alice_id
);
// Bob can still verify Charlie directly.
tcm.execute_securejoin(bob, charlie).await;
let bob_charlie_contact = bob.add_or_lookup_contact(charlie).await;
assert_eq!(
bob_charlie_contact
.get_verifier_id(bob)
.await?
.unwrap()
.unwrap(),
ContactId::SELF
);
Ok(())
}
// ============== Helper Functions ==============
async fn assert_verified(this: &TestContext, other: &TestContext, protected: ProtectionStatus) {
let contact = this.add_or_lookup_contact(other).await;
assert_eq!(contact.is_verified(this).await.unwrap(), true);
if protected != ProtectionStatus::ProtectionBroken {
let contact = this.add_or_lookup_contact(other).await;
assert_eq!(contact.is_verified(this).await.unwrap(), true);
}
let chat = this.get_chat(other).await;
assert_eq!(
chat.is_protected(),
protected == ProtectionStatus::Protected
);
assert_eq!(chat.is_protection_broken(), false);
let (expect_protected, expect_broken) = match protected {
ProtectionStatus::Unprotected => (false, false),
ProtectionStatus::Protected => (true, false),
ProtectionStatus::ProtectionBroken => (false, true),
};
assert_eq!(chat.is_protected(), expect_protected);
assert_eq!(chat.is_protection_broken(), expect_broken);
}
async fn enable_verified_oneonone_chats(test_contexts: &[&TestContext]) {

View File

@@ -1,69 +0,0 @@
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
boundary="18566fe03178296e_f40510c3b59ace3_ecf843ae3ccd2b17"
MIME-Version: 1.0
From: <alice@example.org>
To: <bob@example.net>, <charlie@example.net>
Subject: [...]
Date: Mon, 28 Jul 2025 14:15:14 +0000
Message-ID: <48b9e9cc-2bae-4d41-89b4-a409e2c60c28@localhost>
References: <48b9e9cc-2bae-4d41-89b4-a409e2c60c28@localhost>
Chat-Version: 1.0
Autocrypt: addr=alice@example.org; prefer-encrypt=mutual; keydata=mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5
C0GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz7CkgQQFggAOgUCaIeF8RYhBC5vossjtTLXKGNLWGSw
j2Gp7ZRDAhsDAh4BBQsJCAcCBhUKCQgLAgQWAgMBAScCGQEACgkQZLCPYantlEM66gD/b9qi1/H1Cr
UwwlW2akVX86Q0gX6isyKfuNu/CdTdzaQBAIHRxvwlBNZr56qMGL7CyVy6LmBslLlbQwAdclM9t9UE
uDgEXlh13RIKKwYBBAGXVQEFAQEHQAbtyNbLZIUBTwqeW2W5tVbrusWLJ+nTUmtF7perLbYdAwEIB8
J4BBgWCAAgBQJoh4XxAhsMFiEELm+iyyO1MtcoY0tYZLCPYantlEMACgkQZLCPYantlEPG2QD8DthL
48j1wnjw+Kby7CmAm/M+Me82izk8dGNPn442jJ4A/2r+YmqfUPK2XDXPRwvVBAIz5bL44fe7gNkUUu
XMnzkP
--18566fe03178296e_f40510c3b59ace3_ecf843ae3ccd2b17
Content-Type: application/pgp-encrypted; charset="utf-8"
Content-Description: PGP/MIME version identification
Content-Transfer-Encoding: 7bit
Version: 1
--18566fe03178296e_f40510c3b59ace3_ecf843ae3ccd2b17
Content-Type: application/octet-stream; name="encrypted.asc";
charset="utf-8"
Content-Description: OpenPGP encrypted message
Content-Disposition: inline; filename="encrypted.asc";
Content-Transfer-Encoding: 7bit
-----BEGIN PGP MESSAGE-----
wV4D5tq63hTeebASAQdA5TP3lUjawDdYRzMW0HUyxu0kFUgWtYo+O6pyNE0FJRQw
JKI3nzp0ymh7YKVftd1rc25tWJ2Vjo9ase7H1puGHsfmnY7sqlsMfXrIahM9BGH6
wcBMA+PY3JvEjuMiAQf7BYh7QtxJFimYIj/z8oevEu2YywQhYCl0GFslqDt+45/a
O/49ivcVfr7U0HZoGVTzzrS8+1K//lSj5VLr6pxUNHTNZMIvN1AGRYTpZP4BktYO
uPOhWdNgq30bt4ufxKTRt4mKm2W7OcexyWLkRX19+W2uGm44PP4o3uKQOLJpIZY3
1QKCNCt/JKeF8v3/jmBZqIrtZUC3tB2RwO5MOOjnE2RoE0tLNFvlGJA9TBRvEAhH
4WA+m3kjAeQh0YNG9ujZAgzm8PRvXyiIdhOggpVJ6lVdXXgZMyiglamHdAG8cTcY
VJxI4bR0ZLMXhMqLgLoNfGDpiM75B+5fKJ7U9ICwKcHATAOfNAZQDav2jwEH/0W8
EwR2OuHbB5EphrJ6IisPfE8FrR4BbRs4lMZ6tH1oCrGsoAwr3FtpN9C7o+a0Jp5+
qiYmXDnThipsnMbd0P5OLhZvtG3ZEf9N69hBmDFmWiSJtG+7n1NRlTJ8H9H1r6gO
M07bVqca5bmXiVsTavQ87io0Pr8WGcTUfggqOp3WM1SRNgovE+3ZE8w/ugVIshVz
ri70ZVxAqC1s/0dg9NH4JAnegf8Ds3pRMvSwBaF4EArZ1IPpcNArA592GBKf8BuA
0WGuPSaB/QHSRf9Kar2Wt3/AJXZ8SPCo2S8K/Joc3nuOV2evlKIDeMR9ezbpdWHU
bc5CrZj4U5T3PJx+mLrSwesBPtVHq90EC3+/WpXOd1xlFcVy5CqRVbNOiBFO4fMY
hwnGFFKyK8h8AYU54yXlGdLFeuZSDPTJNueWI917jEIoh2gJm0vJBhdLDhNyPQH8
FwTn6PM29pw5F9Mz4ZEkLqyffGAcWq1CFW8FHDu6pshKFFyPXSolOq+1kbqEgpsV
mYxAZIUJMqrOTAALYyp9kwjjb+O99NCCVNBAjULhG3vme4l8YiAiewQYd5pItcj8
pWI1h+Dp7xWKRrPWVj7TH3gqphRtFm+1X9bw+h4Uz+Qs8bwp7bpIj+SGwEgIDjf5
gefdhpUYf44xcFWALpQJUHNkLXGCs13GXuRUi/eFaDE/cY9JSkbVDPt7BColO5KD
sfzcWxVzE7EqmY8zixR387Fj5BnyjAGm7Kxj7iWkJtngAEKsRbC1PbPUP1O3equs
bM2mMfIBVPep7rWjWHs3uweM2ULUCJkT5wurUR9Pkn+t7jxReNcjWtR9dR0MAksp
GW7qsxIE4EQ86n3X34sR3TNRANIfC1qBFOHa5L5rI6cnyK9e1WSasdRMsZ74e1ot
SySYW0mVZxA/ZV5SYiVWbZVk58PJqdlWasF3blgKwEYjs6AJTpKMBj3YjAnr6CeK
5jWc5wWxyihtRyyriKvFVII0YBbdHF3+W1dnFQGakgWeEPxt1d5zPnizo0V9BewT
BnKlxrj1DtU8n79qRBp4yK2Ks/W/aQxQheEJ00t5ua8aUJTaj6RQqiK5GcH6DU/D
LerLVfRNwDk8mh3YBTOe65ovGa2/WfWXXs/0nHGOhX2ayhd3gtyMnizRohLJZsMK
gYWkF9k6vMBs7mZhm0lobVatYianJkM1cT5DdOtVqfO95K3GOHE7ON9WiZLkmXTH
WEp750aqWjdiu0uveoY7hoAxL+A/IshTQWhXMQ==
=ESpw
-----END PGP MESSAGE-----
--18566fe03178296e_f40510c3b59ace3_ecf843ae3ccd2b17--

View File

@@ -1,52 +0,0 @@
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
boundary="18566fe03178296e_f40510c3b59ace3_ecf843ae3ccd2b17"
MIME-Version: 1.0
From: alice@example.org
To: bob@example.net
Subject: [...]
Date: Mon, 28 Jul 2025 14:15:14 +0000
Message-ID: <48b9e9cc-2bae-4d41-89b4-a409e2c60c28@localhost>
References: <48b9e9cc-2bae-4d41-89b4-a409e2c60c28@localhost>
Chat-Version: 1.0
--18566fe03178296e_f40510c3b59ace3_ecf843ae3ccd2b17
Content-Type: application/pgp-encrypted; charset="utf-8"
Content-Description: PGP/MIME version identification
Content-Transfer-Encoding: 7bit
Version: 1
--18566fe03178296e_f40510c3b59ace3_ecf843ae3ccd2b17
Content-Type: application/octet-stream; name="encrypted.asc";
charset="utf-8"
Content-Description: OpenPGP encrypted message
Content-Disposition: inline; filename="encrypted.asc";
Content-Transfer-Encoding: 7bit
-----BEGIN PGP MESSAGE-----
wV4D5tq63hTeebASAQdAaI0Cw7tTd10Oz0I1C1Ds5NpF/m6zXlx73Pxzib6Q2Qow
36Fc9KiSZx+vXlw9mZ9zYpumIY/svkYcTRdZwohPWe4TL7iRC8gTZ43/VGZvb+1k
wcBMA+PY3JvEjuMiAQf+MCZLm1vgocYO0xRz2J9Z9QGqScfxBMhYToZbFTx4DAha
hejVmW9AGnNqm8yky2DJpqT3oy//D261HX+xzLfkfWFgHzKud7NtMN6II/d1jyqO
A+K0LjSLPcR3aWl8g30+6bGhHCwc2spP3gIk1aE8S/1c/yQPxD7mqNSkYrY6BgHP
+5Z7ocrud4RKVQayZsWpiiY962w1GJii1h4zE+xhkqFERr1OqepQB7CDf4FBsGr0
qarJtcWCGqd9ksr/wn8Ew9MPHz+8ooCSlXzJ7Uac4VshUm9dXzO3NSS6MgTME0av
WRlRiKuGCmrS4dyf/Tdj1yMB3jJ52SxdEq0yYYLKvdLBWAF9JU7sQp3x4oivpr5U
wv5OP/tbfRVA2zqkOHlMhBAXaLOsiJrHYh3ieJLwfWSmEQARLE7sBgD5fqre1v+D
Xpof8R42j9MQ3St/+nPDsLpbZ2a2RKBl16C8IYnmK1CYwh5lEK533HMHeSLka/ng
soK2HqJxrhkxYpm5OPWN8liSdKlQ8mZXISGNPo8KEWFlPqONqj88UudpQiPCh2qw
aeFC2Y1EkQQGpiTq1GuTQfG8zO4wa3FTW1wOELsvYozszS1Oc1exH767pS+ozNWa
xk8J7ekEFvR5b3TUK2a/ucmRgXuuowCxxV6EiNeRqGa3SfQnQWnMkqsVIeIiARDt
QMbRCrrAjJ+dQEekviq/hqq9+DRLuX0hOybe8aYjJttg7NLgaM04V4QuvWeVQa2g
2IBR2Nw1pcvbyDRkTLsQ2AlR/ig5VPf+4oqN2nBuXgPzply9LU5JyhW9GNIhH+N1
K6w8o5o4ebUx0ldaTjBnrFMUqih4nE9qrrb0UYMcQO3HVAueQii0CZqhJy/n/6U6
cAhnh/fTI1wMoYuYR7cLdf+9P7dHcQPdt3FQA/NrRpv31tyzzKNeBr0VdvoT0uwX
sv0nh+ACjnum7H8yCyoe1tIqUPW0byslROuHAfMwp7rsnBEGJ4AqFYNIdpfSFuia
LsZmODaUG1n6XkamfKPin+Z5Mo8/bK0KwE317Zc/aQD/okvGv4SWsQmW
=hj9/
-----END PGP MESSAGE-----
--18566fe03178296e_f40510c3b59ace3_ecf843ae3ccd2b17--

View File

@@ -1,65 +0,0 @@
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
boundary="18566fe03178296e_f40510c3b59ace3_ecf843ae3ccd2b17"
MIME-Version: 1.0
From: <alice@example.org>
To: <bob@example.net>
Subject: [...]
Date: Mon, 28 Jul 2025 14:15:14 +0000
Message-ID: <48b9e9cc-2bae-4d41-89b4-a409e2c60c28@localhost>
References: <48b9e9cc-2bae-4d41-89b4-a409e2c60c28@localhost>
Chat-Version: 1.0
Autocrypt: addr=alice@example.org; prefer-encrypt=mutual; keydata=mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5
C0GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz7CkgQQFggAOgUCaIeF8RYhBC5vossjtTLXKGNLWGSw
j2Gp7ZRDAhsDAh4BBQsJCAcCBhUKCQgLAgQWAgMBAScCGQEACgkQZLCPYantlEM66gD/b9qi1/H1Cr
UwwlW2akVX86Q0gX6isyKfuNu/CdTdzaQBAIHRxvwlBNZr56qMGL7CyVy6LmBslLlbQwAdclM9t9UE
uDgEXlh13RIKKwYBBAGXVQEFAQEHQAbtyNbLZIUBTwqeW2W5tVbrusWLJ+nTUmtF7perLbYdAwEIB8
J4BBgWCAAgBQJoh4XxAhsMFiEELm+iyyO1MtcoY0tYZLCPYantlEMACgkQZLCPYantlEPG2QD8DthL
48j1wnjw+Kby7CmAm/M+Me82izk8dGNPn442jJ4A/2r+YmqfUPK2XDXPRwvVBAIz5bL44fe7gNkUUu
XMnzkP
--18566fe03178296e_f40510c3b59ace3_ecf843ae3ccd2b17
Content-Type: application/pgp-encrypted; charset="utf-8"
Content-Description: PGP/MIME version identification
Content-Transfer-Encoding: 7bit
Version: 1
--18566fe03178296e_f40510c3b59ace3_ecf843ae3ccd2b17
Content-Type: application/octet-stream; name="encrypted.asc";
charset="utf-8"
Content-Description: OpenPGP encrypted message
Content-Disposition: inline; filename="encrypted.asc";
Content-Transfer-Encoding: 7bit
-----BEGIN PGP MESSAGE-----
wV4D5tq63hTeebASAQdAg7f2cyYQy+7Xrrlq4j3ik2Ba7L2sbh7Tt398Kke65Rkw
sUscOFFConkBj7T5D8XS3e9dX5Bnf1z5jTj15OUZx/2iPTRQFtoVoRB6k6vt/qwq
wcBMA+PY3JvEjuMiAQf/U9yB7kWhDMmFI7pMoktqqIvO8woKQG0dx2v1tzjIdtY/
KlqMW8qpAqMHHalInGjj0LDDv4iWnNf8yTh71FyGQgipqH4FnYmbRoFW8iicMixN
0ps6c6tBiZmyWDq2Ub9SIs9L/W2+vDlbyvFnow08MitOniZLC79KXB2ZRFwp7kOm
1gqsZVvsy9fSM7oxXrxAtu2VNyp18emd6jAFWYAz2ISl2KuWYlmSJVexSvQWMRz/
IkKWf9kOVAEuIeUnNo45S/DVb9uWQN3M1TrIdJ1BC+nEDBJokCWUGES6kchJc823
EkLWpn6JDyRYV/7S9tmYrXodO3x4bSL02OnFbyUjutLCRwEduCtRzg5yrw4IAvJO
1ujWf7CaAYE+49oh530HZ/gnBJb03nJhj1SOV0qO9ZquczaW0lhSEtfQF1lVWAVc
BCE44YoR9sBqiJEJ0Msj/WLlso5RZHvHa64JrNJ7Jvisgn5vCMSfInzQ4zIZ7LfD
sR444bJS9V6MNDSuhKmvPvu4wCFZgNQPs4V51yBX8Rjpn/3xws+NpUtisTt5J+ji
KOQg3Thy/9NaNmuXHRbPBxzJKdHzL0bctzVxxDyZPcg6Z2Iteea4gQLEwp5HHw2R
VMX97vtamsjp++tMihXRnrwX/a7x9MCAFuzZted4fB87VjHIdhf+CN2KshWsX+X5
rPR3+oB6EBVXt8IroGMYLTtmMBS4SzEyiGmNFe/Z4tQSU6pEH+Aeo/FmUhUaMhln
BAgRRmhw1Mt9nnuRzLwstpN4W5+mnmccNVg0T6kZz9D7Rbjd7FdzgF8d5K1cJiY/
Nv5aajaFKSEwAO9TNHNoP3LD5KxMPiCkRh888V3YhCOwTUfwJG8riWgeyFCN6Xor
7k6qHhd3T+1u8QTQkooLWSR7UYu9upQzExvmRPNyAXFyLrZUYjlymC1vn9PfH3Pd
31aCGYaYPMdyenoAWTwy7VVSR3wpJuzwHHMeowzCA4TklD/tr2mZSpUrgeBqvS6s
k68Pi5WjMs/kH/3Wl5Octb8XYN++DiG7RH5JzWYRchURen8jgPjzJPIUI5t+C8w0
vXycuP1PdJcSfKTgxkaQgLs5cUoKEAgO5fA9bUPmjEcizb89im6SoObB+6o7hfwa
AIr0TjpOmkdL3TANYA5448gTR4Kq+FwhsxX+fHU6OxwxLBozMcBzvjReKdJko8D+
joaTEZBFxyvQUub5/MXmuulTEDhwURgGMbIN0TukdYlhUBfvyJ/wl/U9aHWvk+dz
3OJ6d9SqTKPPyluTPV7p3GEDy1AwAex5FrP8SxRGRHiMjVhlbwrQB89ZcUX376ge
5MPc4wBn44baPluklYcQtk6kp62KuLpfuLT8VbiLDfKT2FoZzoAnUnw=
=HN9M
-----END PGP MESSAGE-----
--18566fe03178296e_f40510c3b59ace3_ecf843ae3ccd2b17--