mirror of
https://github.com/chatmail/core.git
synced 2026-05-18 14:26:31 +03:00
Compare commits
3 Commits
v2.11.0
...
hoc/channe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ed2a220e7 | ||
|
|
adeb2e0a61 | ||
|
|
ce83952b4f |
26
.github/workflows/ci.yml
vendored
26
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
56
.github/workflows/deltachat-rpc-server.yml
vendored
56
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
2
.github/workflows/jsonrpc.yml
vendored
2
.github/workflows/jsonrpc.yml
vendored
@@ -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
|
||||
|
||||
6
.github/workflows/nix.yml
vendored
6
.github/workflows/nix.yml
vendored
@@ -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
|
||||
|
||||
@@ -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/
|
||||
|
||||
2
.github/workflows/repl.yml
vendored
2
.github/workflows/repl.yml
vendored
@@ -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
|
||||
|
||||
8
.github/workflows/upload-docs.yml
vendored
8
.github/workflows/upload-docs.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/upload-ffi-docs.yml
vendored
2
.github/workflows/upload-ffi-docs.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/zizmor-scan.yml
vendored
2
.github/workflows/zizmor-scan.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
50
CHANGELOG.md
50
CHANGELOG.md
@@ -1,54 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [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
|
||||
@@ -6645,4 +6596,3 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[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
|
||||
|
||||
14
Cargo.lock
generated
14
Cargo.lock
generated
@@ -1285,7 +1285,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "2.11.0"
|
||||
version = "2.10.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-broadcast",
|
||||
@@ -1395,7 +1395,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.11.0"
|
||||
version = "2.10.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.5.0",
|
||||
@@ -1417,7 +1417,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "2.11.0"
|
||||
version = "2.10.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1433,7 +1433,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.11.0"
|
||||
version = "2.10.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1462,7 +1462,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.11.0"
|
||||
version = "2.10.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -3354,9 +3354,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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "2.11.0"
|
||||
version = "2.10.0"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.85"
|
||||
@@ -69,7 +69,7 @@ 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"
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.11.0"
|
||||
version = "2.10.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -7598,18 +7598,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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.11.0"
|
||||
version = "2.10.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.11.0"
|
||||
"version": "2.10.0"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "2.11.0"
|
||||
version = "2.10.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "2.11.0"
|
||||
version = "2.10.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.11.0"
|
||||
version = "2.10.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.11.0"
|
||||
"version": "2.10.0"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "2.11.0"
|
||||
version = "2.10.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.8"
|
||||
|
||||
@@ -1 +1 @@
|
||||
2025-08-13
|
||||
2025-08-04
|
||||
@@ -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
|
||||
|
||||
127
src/chat.rs
127
src/chat.rs
@@ -1771,12 +1771,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 +1780,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() {
|
||||
@@ -1886,25 +1886,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 => {
|
||||
@@ -2499,13 +2490,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
|
||||
}
|
||||
@@ -2916,9 +2905,12 @@ async fn prepare_send_msg(
|
||||
|
||||
let skip_fn = |reason: &CantSendReason| match reason {
|
||||
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
|
||||
// If the chat is a contact request, allow securejoin messages and let the user accept it later.
|
||||
// And allow leaving a contact request chat.
|
||||
matches!(
|
||||
msg.param.get_cmd(),
|
||||
SystemMessage::SecurejoinMessage | SystemMessage::MemberRemovedFromGroup
|
||||
)
|
||||
}
|
||||
// Allow to send "Member removed" messages so we can leave the group/broadcast.
|
||||
// Necessary checks should be made anyway before removing contact
|
||||
@@ -2985,10 +2977,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.
|
||||
@@ -3082,20 +3070,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();
|
||||
@@ -4503,24 +4484,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,
|
||||
@@ -4551,9 +4523,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"
|
||||
@@ -4562,7 +4543,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
|
||||
@@ -4573,21 +4563,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(
|
||||
|
||||
@@ -739,7 +739,6 @@ async fn test_leave_group() -> Result<()> {
|
||||
|
||||
tcm.section("Bob leaves the group.");
|
||||
let bob_chat_id = bob_msg.chat_id;
|
||||
bob_chat_id.accept(&bob).await?;
|
||||
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
|
||||
|
||||
let leave_msg = bob.pop_sent_msg().await;
|
||||
@@ -2280,19 +2279,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?;
|
||||
@@ -2310,8 +2304,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!(
|
||||
@@ -2982,7 +2974,6 @@ async fn test_leave_broadcast() -> Result<()> {
|
||||
|
||||
tcm.section("Bob leaves the broadcast channel.");
|
||||
let bob_chat_id = bob_msg.chat_id;
|
||||
bob_chat_id.accept(bob).await?;
|
||||
remove_contact_from_chat(bob, bob_chat_id, ContactId::SELF).await?;
|
||||
|
||||
let leave_msg = bob.pop_sent_msg().await;
|
||||
@@ -4779,25 +4770,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(())
|
||||
}
|
||||
|
||||
@@ -807,28 +807,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 id>?2 AND origin>=?3 AND (? OR blocked=?)
|
||||
ORDER BY last_seen DESC LIMIT 1",
|
||||
(
|
||||
&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 +1550,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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1322,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"
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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;
|
||||
@@ -630,7 +630,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)
|
||||
@@ -1271,15 +1272,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 {
|
||||
@@ -1400,6 +1397,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(),
|
||||
@@ -1569,6 +1567,7 @@ async fn do_chat_assignment(
|
||||
context,
|
||||
mime_parser,
|
||||
to_ids,
|
||||
from_id,
|
||||
allow_creation,
|
||||
Blocked::Not,
|
||||
is_partial_download.is_some(),
|
||||
@@ -1673,12 +1672,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.
|
||||
@@ -1699,6 +1698,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(),
|
||||
@@ -1863,8 +1863,10 @@ async fn add_parts(
|
||||
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)
|
||||
@@ -1875,14 +1877,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,
|
||||
@@ -2139,10 +2139,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
|
||||
},
|
||||
@@ -2468,6 +2464,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,
|
||||
@@ -2490,29 +2487,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);
|
||||
@@ -2877,13 +2855,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,
|
||||
@@ -3808,16 +3793,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(
|
||||
@@ -3854,26 +3836,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)
|
||||
@@ -3981,12 +3948,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;
|
||||
|
||||
@@ -5091,44 +5091,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;
|
||||
@@ -5390,99 +5352,3 @@ async fn test_group_introduction_no_gossip() -> Result<()> {
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
#[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(())
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
37
src/sql.rs
37
src/sql.rs
@@ -13,7 +13,6 @@ use crate::config::Config;
|
||||
use crate::constants::DC_CHAT_ID_TRASH;
|
||||
use crate::context::Context;
|
||||
use crate::debug_logging::set_debug_logging_xdc;
|
||||
use crate::ensure_and_debug_assert;
|
||||
use crate::ephemeral::start_ephemeral_timers;
|
||||
use crate::imex::BLOBS_BACKUP_NAME;
|
||||
use crate::location::delete_orphaned_poi_locations;
|
||||
@@ -176,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::new();
|
||||
for _ in 0..Self::N_DB_CONNECTIONS {
|
||||
for _ in 0..3 {
|
||||
let connection = new_connection(dbfile, &passphrase)?;
|
||||
connections.push(connection);
|
||||
}
|
||||
@@ -640,31 +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(&self) -> Result<()> {
|
||||
let lock = self.pool.read().await;
|
||||
let pool = lock.as_ref().context("No SQL connection pool")?;
|
||||
let mut conns = Vec::new();
|
||||
let query_only = true;
|
||||
// Kick out readers to avoid blocking/SQLITE_BUSY.
|
||||
for _ in 0..(Self::N_DB_CONNECTIONS - 1) {
|
||||
conns.push(pool.get(query_only).await?);
|
||||
}
|
||||
let conn = pool.get(query_only).await?;
|
||||
tokio::task::block_in_place(move || {
|
||||
// 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", [], |_row| Ok(()))?;
|
||||
let blocked = conn.query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |row| {
|
||||
let blocked: i64 = row.get(0)?;
|
||||
Ok(blocked)
|
||||
})?;
|
||||
ensure_and_debug_assert!(blocked == 0,);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new SQLite connection.
|
||||
@@ -788,13 +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) = context.sql.wal_checkpoint().await {
|
||||
warn!(context, "wal_checkpoint() failed: {err:#}.");
|
||||
}
|
||||
|
||||
context
|
||||
.sql
|
||||
|
||||
@@ -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,19 @@ 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.is_key_contact(), false);
|
||||
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 +64,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,9 +82,8 @@ 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);
|
||||
@@ -116,23 +112,12 @@ 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 +146,16 @@ 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.is_key_contact(), false);
|
||||
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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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--
|
||||
Reference in New Issue
Block a user