mirror of
https://github.com/chatmail/core.git
synced 2026-04-04 14:32:11 +03:00
Compare commits
285 Commits
pre-main
...
cli-displa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18044c2fef | ||
|
|
f5dea1d252 | ||
|
|
8dd7c42f69 | ||
|
|
b542eeecc0 | ||
|
|
bee8295daa | ||
|
|
ab9fd3d5ed | ||
|
|
cc54a3feda | ||
|
|
94984f35ec | ||
|
|
0e47e89d63 | ||
|
|
2d7dc7a1be | ||
|
|
4d76a5b599 | ||
|
|
87035ff744 | ||
|
|
e0d123f732 | ||
|
|
8eddcfc9d2 | ||
|
|
af58b86b60 | ||
|
|
00ae7ce33c | ||
|
|
0bc9fe841a | ||
|
|
e37920ed4e | ||
|
|
6a7466df93 | ||
|
|
1bb966e5a8 | ||
|
|
34e631395f | ||
|
|
080ddde68d | ||
|
|
209a8026fb | ||
|
|
23bfa4fc43 | ||
|
|
58d40c118c | ||
|
|
9d39769445 | ||
|
|
bfc08abe88 | ||
|
|
6a7b097273 | ||
|
|
8f2390ac99 | ||
|
|
481f5cae22 | ||
|
|
b9068b95b8 | ||
|
|
df2c35b551 | ||
|
|
3cd4152a3c | ||
|
|
2534510f0b | ||
|
|
3f8aa4635e | ||
|
|
ada59e8205 | ||
|
|
9ec0332483 | ||
|
|
d509b0cf5c | ||
|
|
4d624d8c3a | ||
|
|
9f0ba4b9c2 | ||
|
|
a930ae27be | ||
|
|
38e4919be1 | ||
|
|
a668047f75 | ||
|
|
c2ea2cda4c | ||
|
|
f3c3a2c301 | ||
|
|
0da7e587a7 | ||
|
|
e6e686aaf4 | ||
|
|
58e1fa5c36 | ||
|
|
42549526c7 | ||
|
|
9fe1c8fe80 | ||
|
|
b8dbcb3dbd | ||
|
|
7c5675670a | ||
|
|
291945a4fd | ||
|
|
439e8827bd | ||
|
|
a745cf78ee | ||
|
|
af69756df0 | ||
|
|
46c42ab6e4 | ||
|
|
33a127187b | ||
|
|
24ddbdd251 | ||
|
|
0122a98eea | ||
|
|
406545c1f1 | ||
|
|
a1b593027b | ||
|
|
eae1ba258a | ||
|
|
d2db30eabc | ||
|
|
9fb7c52217 | ||
|
|
6cab1786d3 | ||
|
|
362328167c | ||
|
|
570a9993f7 | ||
|
|
5adc68cf0b | ||
|
|
1b1757ebf2 | ||
|
|
d8950fb7d1 | ||
|
|
ba2e573c23 | ||
|
|
31391fc074 | ||
|
|
f94b2c3794 | ||
|
|
eb0a5fed8e | ||
|
|
eaa47d175f | ||
|
|
e968000a89 | ||
|
|
1ba448fe19 | ||
|
|
a5c82425f4 | ||
|
|
1bd31f6b8e | ||
|
|
c0ea0e52b3 | ||
|
|
e6a3daacb3 | ||
|
|
09dabda4a3 | ||
|
|
f523d912af | ||
|
|
90b0ca79ea | ||
|
|
a506e2d5a2 | ||
|
|
4c66518a68 | ||
|
|
42b4b83f8e | ||
|
|
7477ebbdd7 | ||
|
|
738dc5ce19 | ||
|
|
3680467e14 | ||
|
|
c5ada9b203 | ||
|
|
3d2805bc78 | ||
|
|
2dde286d68 | ||
|
|
2260156c40 | ||
|
|
129e970727 | ||
|
|
66271db8c0 | ||
|
|
09d33e62bd | ||
|
|
bf3dfa4ab6 | ||
|
|
40b866117e | ||
|
|
cb5f9f3051 | ||
|
|
80f97cf9bd | ||
|
|
6d860f7eae | ||
|
|
545643b610 | ||
|
|
7ee6f2c36a | ||
|
|
5d9b887624 | ||
|
|
12c0e298f5 | ||
|
|
f9aec7af0d | ||
|
|
b181d78dd5 | ||
|
|
b9ff40c6b5 | ||
|
|
0684810d38 | ||
|
|
1cc7ce6e27 | ||
|
|
82bc1bf0b1 | ||
|
|
75bcf8660b | ||
|
|
5e1d945198 | ||
|
|
e047184ede | ||
|
|
307a2eb6ec | ||
|
|
ab8aedf06e | ||
|
|
b6ab13f1de | ||
|
|
53a3e51920 | ||
|
|
4033566b4a | ||
|
|
bed1623dcb | ||
|
|
d4704977bc | ||
|
|
838eed94bc | ||
|
|
9870725d1f | ||
|
|
ba827283be | ||
|
|
1e37cb8c3c | ||
|
|
1991e01641 | ||
|
|
d7e87b6336 | ||
|
|
fde490ba15 | ||
|
|
cf5a16d967 | ||
|
|
e8dde9c63d | ||
|
|
667a935665 | ||
|
|
28cea706fa | ||
|
|
209a990444 | ||
|
|
6365a46fac | ||
|
|
a81496e9ab | ||
|
|
ca05733b9d | ||
|
|
dfb5348a78 | ||
|
|
602e52490c | ||
|
|
740b24e8a4 | ||
|
|
44a09ffd12 | ||
|
|
054c42cbc2 | ||
|
|
34263a70e2 | ||
|
|
7ea6ca35d7 | ||
|
|
a9aad497fc | ||
|
|
7da8489635 | ||
|
|
683561374d | ||
|
|
66c9982822 | ||
|
|
1b6450b210 | ||
|
|
aa8a13adb2 | ||
|
|
5888541c05 | ||
|
|
f893487dc0 | ||
|
|
b84beaf974 | ||
|
|
75a3c55e70 | ||
|
|
854a09e12f | ||
|
|
40412fd4a9 | ||
|
|
57fc084795 | ||
|
|
143ba6d5e7 | ||
|
|
6b338a923c | ||
|
|
e6ab1e3df5 | ||
|
|
5da6976bf9 | ||
|
|
bd15d90e77 | ||
|
|
61633cf23b | ||
|
|
9f1107c0e7 | ||
|
|
ff0d5ce179 | ||
|
|
0bbd910883 | ||
|
|
4258088fb4 | ||
|
|
6372b677d2 | ||
|
|
9af00af70f | ||
|
|
4010c60e7b | ||
|
|
aaa83a8f52 | ||
|
|
776408c564 | ||
|
|
d0cb2110e6 | ||
|
|
11e3480fe8 | ||
|
|
2cd54b72b0 | ||
|
|
c34ccafb2e | ||
|
|
6837874d43 | ||
|
|
3656337d41 | ||
|
|
a89b6321f1 | ||
|
|
ac10103b18 | ||
|
|
b696a242fc | ||
|
|
7e4822c8ca | ||
|
|
a955cb5400 | ||
|
|
2e2cfc4cb3 | ||
|
|
4157d1986f | ||
|
|
d13eb2f580 | ||
|
|
5476f69179 | ||
|
|
dcdf30da35 | ||
|
|
55746c8c19 | ||
|
|
dbdf5f2746 | ||
|
|
b4e28deed3 | ||
|
|
f4a604dcfb | ||
|
|
b3c5787ec8 | ||
|
|
471d0469dd | ||
|
|
113eda575f | ||
|
|
45f1da82fe | ||
|
|
5f45ff77e4 | ||
|
|
1c0201ee3d | ||
|
|
c7340e04ec | ||
|
|
0a32476dc5 | ||
|
|
e02bc6ffb5 | ||
|
|
f41a3970b2 | ||
|
|
6c536f3a9b | ||
|
|
4b24b6a848 | ||
|
|
5f254a929f | ||
|
|
8df1a01ace | ||
|
|
27b5ffb34f | ||
|
|
80af012962 | ||
|
|
615c80bef4 | ||
|
|
f5f4026dbb | ||
|
|
b431206ede | ||
|
|
c4878e9b49 | ||
|
|
aa452971a6 | ||
|
|
2d798f7cfe | ||
|
|
08bb0484eb | ||
|
|
b0b7337f5a | ||
|
|
93241a4beb | ||
|
|
4f1bf1f13c | ||
|
|
2d0b7b5bd8 | ||
|
|
8fe3ce5cab | ||
|
|
59a0f1d94f | ||
|
|
5175dc3450 | ||
|
|
9a22ccd058 | ||
|
|
c06ed49a2a | ||
|
|
2e51a5a454 | ||
|
|
75cc353528 | ||
|
|
3977580426 | ||
|
|
3a1370e174 | ||
|
|
c218c05b96 | ||
|
|
db247d9f9a | ||
|
|
78b7715ea6 | ||
|
|
ba76944d75 | ||
|
|
4a1a2122f0 | ||
|
|
d80b749dec | ||
|
|
039a8b7c36 | ||
|
|
779f58ab16 | ||
|
|
b9183fe5eb | ||
|
|
9d342671d5 | ||
|
|
4e47ebd5fc | ||
|
|
d5c418e909 | ||
|
|
85414558c5 | ||
|
|
d6af8d2526 | ||
|
|
1209e95e34 | ||
|
|
51f9279e67 | ||
|
|
f27d54f7fa | ||
|
|
7f3648f8ae | ||
|
|
49fc258578 | ||
|
|
0c51b4fe41 | ||
|
|
dbad714539 | ||
|
|
edd8008650 | ||
|
|
615a1b3f4e | ||
|
|
fe6044e1aa | ||
|
|
46b275bfab | ||
|
|
25f44c517a | ||
|
|
cac04f8ee4 | ||
|
|
45d8566ec0 | ||
|
|
29a98ba13b | ||
|
|
e3973f6448 | ||
|
|
7b41425fe4 | ||
|
|
2c7d51f98f | ||
|
|
a2df29515a | ||
|
|
6df1d165dd | ||
|
|
e03e2d9a68 | ||
|
|
8fc6ea19b4 | ||
|
|
c5c947e175 | ||
|
|
6d8dff54a7 | ||
|
|
a0f6bdffeb | ||
|
|
e6fd52afff | ||
|
|
0142515887 | ||
|
|
d45ec7f34d | ||
|
|
752f45f0f0 | ||
|
|
0299543a86 | ||
|
|
d3908d6b36 | ||
|
|
2cf979de53 | ||
|
|
f5e8c8083d | ||
|
|
58b99f59f7 | ||
|
|
402e42f858 | ||
|
|
fbae0739a6 | ||
|
|
0359481ba4 | ||
|
|
6406f305b8 | ||
|
|
e5e0f0cdd7 | ||
|
|
0bac4acdd8 | ||
|
|
ce5697c5f7 | ||
|
|
22258f7269 |
46
.github/workflows/ci.yml
vendored
46
.github/workflows/ci.yml
vendored
@@ -20,7 +20,7 @@ permissions: {}
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
RUST_VERSION: 1.88.0
|
||||
RUST_VERSION: 1.90.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@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
name: cargo deny
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -67,10 +67,12 @@ jobs:
|
||||
name: Check provider database
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- name: Install rustfmt
|
||||
run: rustup component add --toolchain stable-x86_64-unknown-linux-gnu rustfmt
|
||||
- name: Check provider database
|
||||
run: scripts/update-provider-database.sh
|
||||
|
||||
@@ -80,7 +82,7 @@ jobs:
|
||||
env:
|
||||
RUSTDOCFLAGS: -Dwarnings
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -115,7 +117,7 @@ jobs:
|
||||
shell: bash
|
||||
if: matrix.rust == 'latest'
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -137,12 +139,12 @@ jobs:
|
||||
- name: Tests
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
run: cargo nextest run --workspace
|
||||
run: cargo nextest run --workspace --locked
|
||||
|
||||
- name: Doc-Tests
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
run: cargo test --workspace --doc
|
||||
run: cargo test --workspace --locked --doc
|
||||
|
||||
- name: Test cargo vendor
|
||||
run: cargo vendor
|
||||
@@ -154,7 +156,7 @@ jobs:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -179,7 +181,7 @@ jobs:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -201,7 +203,7 @@ jobs:
|
||||
name: Python lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -226,9 +228,9 @@ jobs:
|
||||
include:
|
||||
# Currently used Rust version.
|
||||
- os: ubuntu-latest
|
||||
python: 3.13
|
||||
python: 3.14
|
||||
- os: macos-latest
|
||||
python: 3.13
|
||||
python: 3.14
|
||||
|
||||
# PyPy tests
|
||||
- os: ubuntu-latest
|
||||
@@ -244,19 +246,19 @@ jobs:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download libdeltachat.a
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: ${{ matrix.os }}-libdeltachat.a
|
||||
path: target/debug
|
||||
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
|
||||
@@ -279,11 +281,11 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
python: 3.13
|
||||
python: 3.14
|
||||
- os: macos-latest
|
||||
python: 3.13
|
||||
python: 3.14
|
||||
- os: windows-latest
|
||||
python: 3.13
|
||||
python: 3.14
|
||||
|
||||
# PyPy tests
|
||||
- os: ubuntu-latest
|
||||
@@ -297,13 +299,13 @@ jobs:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
|
||||
@@ -311,7 +313,7 @@ jobs:
|
||||
run: pip install tox
|
||||
|
||||
- name: Download deltachat-rpc-server
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: ${{ matrix.os }}-deltachat-rpc-server
|
||||
path: target/debug
|
||||
|
||||
70
.github/workflows/deltachat-rpc-server.yml
vendored
70
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -30,11 +30,11 @@ jobs:
|
||||
arch: [aarch64, armv7l, armv6l, i686, x86_64]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||
@@ -54,11 +54,11 @@ jobs:
|
||||
arch: [win32, win64]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -105,11 +105,11 @@ jobs:
|
||||
arch: [arm64-v8a, armeabi-v7a]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
|
||||
@@ -132,74 +132,74 @@ jobs:
|
||||
contents: write
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
|
||||
|
||||
- name: Download Linux aarch64 binary
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-aarch64-linux
|
||||
path: deltachat-rpc-server-aarch64-linux.d
|
||||
|
||||
- name: Download Linux armv7l binary
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-armv7l-linux
|
||||
path: deltachat-rpc-server-armv7l-linux.d
|
||||
|
||||
- name: Download Linux armv6l binary
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-armv6l-linux
|
||||
path: deltachat-rpc-server-armv6l-linux.d
|
||||
|
||||
- name: Download Linux i686 binary
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-i686-linux
|
||||
path: deltachat-rpc-server-i686-linux.d
|
||||
|
||||
- name: Download Linux x86_64 binary
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64-linux
|
||||
path: deltachat-rpc-server-x86_64-linux.d
|
||||
|
||||
- name: Download Win32 binary
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-win32
|
||||
path: deltachat-rpc-server-win32.d
|
||||
|
||||
- name: Download Win64 binary
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-win64
|
||||
path: deltachat-rpc-server-win64.d
|
||||
|
||||
- name: Download macOS binary for x86_64
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
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@v4
|
||||
uses: actions/download-artifact@v5
|
||||
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@v4
|
||||
uses: actions/download-artifact@v5
|
||||
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@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-armeabi-v7a-android
|
||||
path: deltachat-rpc-server-armeabi-v7a-android.d
|
||||
@@ -224,7 +224,7 @@ jobs:
|
||||
|
||||
# Python 3.11 is needed for tomllib used in scripts/wheel-rpc-server.py
|
||||
- name: Install python 3.12
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.12
|
||||
|
||||
@@ -285,76 +285,76 @@ jobs:
|
||||
# Needed to publish the binaries to the release.
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Download Linux aarch64 binary
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-aarch64-linux
|
||||
path: deltachat-rpc-server-aarch64-linux.d
|
||||
|
||||
- name: Download Linux armv7l binary
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-armv7l-linux
|
||||
path: deltachat-rpc-server-armv7l-linux.d
|
||||
|
||||
- name: Download Linux armv6l binary
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-armv6l-linux
|
||||
path: deltachat-rpc-server-armv6l-linux.d
|
||||
|
||||
- name: Download Linux i686 binary
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-i686-linux
|
||||
path: deltachat-rpc-server-i686-linux.d
|
||||
|
||||
- name: Download Linux x86_64 binary
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64-linux
|
||||
path: deltachat-rpc-server-x86_64-linux.d
|
||||
|
||||
- name: Download Win32 binary
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-win32
|
||||
path: deltachat-rpc-server-win32.d
|
||||
|
||||
- name: Download Win64 binary
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-win64
|
||||
path: deltachat-rpc-server-win64.d
|
||||
|
||||
- name: Download macOS binary for x86_64
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
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@v4
|
||||
uses: actions/download-artifact@v5
|
||||
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@v4
|
||||
uses: actions/download-artifact@v5
|
||||
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@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-armeabi-v7a-android
|
||||
path: deltachat-rpc-server-armeabi-v7a-android.d
|
||||
@@ -401,7 +401,7 @@ jobs:
|
||||
deltachat-rpc-server/npm-package/*.tgz
|
||||
|
||||
# Configure Node.js for publishing.
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
@@ -14,12 +14,12 @@ jobs:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
4
.github/workflows/jsonrpc.yml
vendored
4
.github/workflows/jsonrpc.yml
vendored
@@ -16,12 +16,12 @@ jobs:
|
||||
build_and_test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 18.x
|
||||
- name: Add Rust cache
|
||||
|
||||
19
.github/workflows/nix.yml
vendored
19
.github/workflows/nix.yml
vendored
@@ -5,10 +5,12 @@ on:
|
||||
paths:
|
||||
- flake.nix
|
||||
- flake.lock
|
||||
- .github/workflows/nix.yml
|
||||
push:
|
||||
paths:
|
||||
- flake.nix
|
||||
- flake.lock
|
||||
- .github/workflows/nix.yml
|
||||
branches:
|
||||
- main
|
||||
|
||||
@@ -19,15 +21,12 @@ jobs:
|
||||
name: check flake formatting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- run: nix fmt
|
||||
|
||||
# Check that formatting does not change anything.
|
||||
- run: git diff --exit-code
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
|
||||
- run: nix fmt flake.nix -- --check
|
||||
|
||||
build:
|
||||
name: nix build
|
||||
@@ -81,11 +80,11 @@ jobs:
|
||||
#- deltachat-rpc-server-x86_64-android
|
||||
#- deltachat-rpc-server-x86-android
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
build-macos:
|
||||
@@ -101,9 +100,9 @@ jobs:
|
||||
# - deltachat-rpc-server-aarch64-darwin
|
||||
# - deltachat-rpc-server-x86_64-darwin
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Download all the dists
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: python-package-distributions
|
||||
path: dist/
|
||||
|
||||
4
.github/workflows/repl.yml
vendored
4
.github/workflows/repl.yml
vendored
@@ -14,11 +14,11 @@ jobs:
|
||||
name: Build REPL example
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
|
||||
- name: Build
|
||||
run: nix build .#deltachat-repl-win64
|
||||
- name: Upload binary
|
||||
|
||||
14
.github/workflows/upload-docs.yml
vendored
14
.github/workflows/upload-docs.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -31,12 +31,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
|
||||
- name: Build Python documentation
|
||||
run: nix build .#python-docs
|
||||
- name: Upload to py.delta.chat
|
||||
@@ -50,12 +50,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
|
||||
- name: Build C documentation
|
||||
run: nix build .#docs
|
||||
- name: Upload to c.delta.chat
|
||||
@@ -72,13 +72,13 @@ jobs:
|
||||
working-directory: ./deltachat-jsonrpc/typescript
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '18'
|
||||
- name: npm install
|
||||
|
||||
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@v4
|
||||
- uses: actions/checkout@v5
|
||||
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@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
505
CHANGELOG.md
505
CHANGELOG.md
@@ -1,5 +1,480 @@
|
||||
# Changelog
|
||||
|
||||
## [2.20.0] - 2025-10-13
|
||||
|
||||
This release fixes a bug that resulted in ephemeral loop getting stuck in infinite loop
|
||||
when trying to delete a message with unknown viewtype.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Accept unknown viewtype in ephemeral loop.
|
||||
- Accept unknown viewtype in delete-old-messages loop.
|
||||
|
||||
## [2.19.0] - 2025-10-12
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Slightly increase saturation of colors.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not fail to receive call accepted/ended messages referring to non-call Message-ID.
|
||||
- Do not fail to fully download previously trashed messages.
|
||||
- Emit AccountsItemChanged when own key is generated/imported, use gray self-color until that ([#7296](https://github.com/chatmail/core/pull/7296)).
|
||||
- Do not try to process calls from partial messages.
|
||||
|
||||
### CI
|
||||
|
||||
- Update to Python 3.14.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Use variables directly in formatted strings ([#7284](https://github.com/chatmail/core/pull/7284)).
|
||||
- Set_chat_profile_image(): Remove !chat.is_mailing_list() check.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Bump quick-xml from 0.37.5 to 0.38.3.
|
||||
- Add nodejs to nix dev env ([#7283](https://github.com/chatmail/core/pull/7283))
|
||||
|
||||
## [2.18.0] - 2025-10-08
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Remove APIs for video chat invitations.
|
||||
|
||||
### CI
|
||||
|
||||
- nix: Run the workflow when workflow file changes.
|
||||
- nix: Switch from DeterminateSystems/nix-installer-action to cachix/install-nix-action.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- No implicit member changes from old Delta Chat clients ([#7220](https://github.com/chatmail/core/pull/7220)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not fail to load messages with unknown viewtype.
|
||||
- Only omit group changes messages if SELF is really added ([#7220](https://github.com/chatmail/core/pull/7220)).
|
||||
|
||||
### Refactor
|
||||
|
||||
- Assert that Iroh node addresses have home relay URL.
|
||||
|
||||
## [2.17.0] - 2025-10-04
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Remove deprecated verified_one_on_one_chats config.
|
||||
|
||||
### CI
|
||||
|
||||
- Require that Cargo.lock is up to date.
|
||||
- Fix CI checking Nix formatting.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Comment about outdated timespan.
|
||||
- Clarify CALL events ([#7188](https://github.com/chatmail/core/pull/7188)).
|
||||
- Add docs for JS `BaseDeltaChat`.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Make `text/calendar` alternative available as an attachment.
|
||||
- Better summary for calls.
|
||||
- Add strings 'You left the channel.' and 'Scan to join Channel' ([#7266](https://github.com/chatmail/core/pull/7266)).
|
||||
- Stock strings for calls.
|
||||
- ffi: Add DC_STR_CANT_DECRYPT_OUTGOING_MSGS define.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Prefer last part in `multipart/alternative`.
|
||||
- Prefetch messages in limited batches ([#6915](https://github.com/chatmail/core/pull/6915)).
|
||||
- Forward calls as text messages.
|
||||
- Consistent spelling of "canceled" with a single "l".
|
||||
- Lowercase "call" in "Missed call" and similar strings.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Return the reason when failing to place calls.
|
||||
|
||||
### Tests
|
||||
|
||||
- Test reception of `multipart/alternative` with `text/calendar`.
|
||||
|
||||
## [2.16.0] - 2025-10-01
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Get rid of inviter progress other than 0 and 1000.
|
||||
- Add has_video attribute to incoming call events.
|
||||
- Add JSON-RPC API to get ICE servers.
|
||||
- Add call_info() JSON-RPC API.
|
||||
- Add chat ID to SecureJoinInviterProgress.
|
||||
- deltachat-rpc-client: Add Chat.resend_messages().
|
||||
- Add `chat_id` to all call events ([#7216](https://github.com/chatmail/core/pull/7216)).
|
||||
|
||||
### Build system
|
||||
|
||||
- Update rPGP from 0.16.0 to 0.17.0.
|
||||
|
||||
### CI
|
||||
|
||||
- Update Rust to 1.90.0.
|
||||
- Install rustfmt before checking provider database.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add more `get_next_event` docs.
|
||||
- SecurejoinInviterProgress never returns an error.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Don't fetch messages from unknown folders ([#7190](https://github.com/chatmail/core/pull/7190)).
|
||||
- Get ICE servers from IMAP METADATA.
|
||||
- Don't ignore receive_imf_inner() errors, try adding partially downloaded message instead ([#7196](https://github.com/chatmail/core/pull/7196)).
|
||||
- Set dimensions for outgoing Sticker messages.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Create 1:1 chat only if auth token is for setup contact.
|
||||
- Ignore vc-/vg- prefix for SecurejoinInviterProgress.
|
||||
- Don't init Iroh on channel leave ([#7210](https://github.com/chatmail/core/pull/7210)).
|
||||
- Take the last valid Autocrypt header ([#7167](https://github.com/chatmail/core/pull/7167)).
|
||||
- Don't add "member removed" messages from nonmembers ([#7207](https://github.com/chatmail/core/pull/7207)).
|
||||
- Do not consider the call stale if it is not sent out yet.
|
||||
- Receive_imf: Report replaced message id in `MsgsChanged` if chat is the same.
|
||||
- Allow Exif for stickers, don't recode them because of that ([#6447](https://github.com/chatmail/core/pull/6447)).
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove unused prop (TS, `BaseDeltaChat`).
|
||||
- Remove unused FolderMeaning::Drafts.
|
||||
|
||||
### Tests
|
||||
|
||||
- Rename test_udpate_call_text into test_update_call_text.
|
||||
- Update timestamp_sent in pop_sent_msg_opt().
|
||||
- Do not match call ID from second alice with first alice event.
|
||||
|
||||
## [2.15.0] - 2025-09-15
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add JSON-RPC API for calls ([#7194](https://github.com/chatmail/core/pull/7194)).
|
||||
|
||||
### Build system
|
||||
|
||||
- Remove unused `quoted_printable` dependency.
|
||||
|
||||
## [2.14.0] - 2025-09-12
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Put the chattype into the SecurejoinInviterProgress event ([#7181](https://github.com/chatmail/core/pull/7181)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- param: Split params only on \n.
|
||||
- B-encode SDP offer and answer sent in headers.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Use recv_msg_trash() instead of recv_msg_opt().
|
||||
- Prepare_msg_raw(): don't return MsgId.
|
||||
|
||||
### Tests
|
||||
|
||||
- Message is OutFailed if all keys are missing ([#6849](https://github.com/chatmail/core/pull/6849)).
|
||||
- Test sending SDP offer and answer with newlines.
|
||||
|
||||
## [2.13.0] - 2025-09-09
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Remove `is_profile_verified` APIs.
|
||||
- [**breaking**] Remove deprecated `is_protection_broken`.
|
||||
- [**breaking**] Remove `e2ee_enabled` preference.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add call ringing API ([#6650](https://github.com/chatmail/core/pull/6650), [#7174](https://github.com/chatmail/core/pull/7174), [#7175](https://github.com/chatmail/core/pull/7175), [#7179](https://github.com/chatmail/core/pull/7179))
|
||||
- Warn for outdated versions after 6 months instead of 1 year ([#7144](https://github.com/chatmail/core/pull/7144)).
|
||||
- Do not set "unknown sender for this chat" error.
|
||||
- Do not replace messages with an error on verification failure.
|
||||
- Support receiving Autocrypt-Gossip with `_verified` attribute.
|
||||
- Withdraw all QR codes when one is withdrawn.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Don't reverify contacts by SELF on receipt of a message from another device.
|
||||
- Don't verify contacts by others having an unknown verifier.
|
||||
- Update verifier_id if it's "unknown" and new verifier has known verifier.
|
||||
- Mark message as failed if it can't be sent ([#7143](https://github.com/chatmail/core/pull/7143)).
|
||||
- Add "Messages are end-to-end encrypted." to non-protected groups.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Fix for SecurejoinInviterProgress with progress == 600.
|
||||
- STYLE.md: Prefer BTreeMap and BTreeSet over hash variants.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Update provider database.
|
||||
- Update dependencies.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Check that verifier is verified in turn.
|
||||
- Remove unused `EncryptPreference::Reset`.
|
||||
- Remove `Aheader::new`.
|
||||
|
||||
### Tests
|
||||
|
||||
- Add another TimeShiftFalsePositiveNote ([#7142](https://github.com/chatmail/core/pull/7142)).
|
||||
- Add TestContext.create_chat_id.
|
||||
|
||||
## [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
|
||||
|
||||
- Fix crash when receiving a verification-gossiping message which a contact also sends to itself ([#7032](https://github.com/chatmail/core/pull/7032)).
|
||||
|
||||
## [2.5.0] - 2025-07-22
|
||||
|
||||
### Fixes
|
||||
|
||||
- Correctly migrate "verified by me".
|
||||
- Mark all email chats as unprotected in the migration ([#7026](https://github.com/chatmail/core/pull/7026)).
|
||||
- Do not ignore errors in add_flag_finalized_with_set.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Deprecate protection-broken and related stuff ([#7018](https://github.com/chatmail/core/pull/7018)).
|
||||
- Clarify the meaning of is_verified() vs verifier_id() ([#7027](https://github.com/chatmail/core/pull/7027)).
|
||||
- STYLE.md: Prefer `try_next()` over `next()`.
|
||||
|
||||
## [2.4.0] - 2025-07-21
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not ignore errors when draining FETCH responses. This avoids IMAP loop getting stuck in an infinite loop retrying reading from the connection.
|
||||
- Update `tokio-io-timeout` to 1.2.1. This release includes a fix to reset timeout after every error, so timeout error is returned at most once a minute if read is attempted after a timeout.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Update async-imap to 0.11.0.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Use `try_next()` when processing FETCH responses.
|
||||
|
||||
## [2.3.0] - 2025-07-19
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add "e2ee encrypted" info message to all e2ee chats ([#7008](https://github.com/chatmail/core/pull/7008)).
|
||||
- repl: Print errors and debug logs to stderr.
|
||||
- `{ensure_and,logged}_debug_assert`: Don't evaluate condition twice.
|
||||
- Log when background fetch of all accounts finishes successfully.
|
||||
- Log the number of read/written bytes on IMAP stream read error ([#6924](https://github.com/chatmail/core/pull/6924)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Ignore protected headers in outer message part ([#6357](https://github.com/chatmail/core/pull/6357)).
|
||||
- List e-mail contacts in repl listcontacts command.
|
||||
- Save peer address for LoggingStream early.
|
||||
|
||||
## [2.2.0] - 2025-07-14
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add chat::create_group_ex(), deprecate create_group_chat() ([#6927](https://github.com/chatmail/core/pull/6927)).
|
||||
- jsonrpc: Add CommandApi::create_group_chat_unencrypted() ([#6927](https://github.com/chatmail/core/pull/6927)).
|
||||
- [**breaking**] In ChatListItem, replace is_group and is_(out_)broadcast with chat_type property ([#7003](https://github.com/chatmail/core/pull/7003)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Log failed debug assertions in all configurations.
|
||||
- Donation request device message ([#6913](https://github.com/chatmail/core/pull/6913)).
|
||||
- Advance next UID even if connection fails while fetching.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Always prefer the last header.
|
||||
|
||||
### Tests
|
||||
|
||||
- Tune down DELTACHAT_SAVE_TMP_DB hint ([#6998](https://github.com/chatmail/core/pull/6998)).
|
||||
- Unencrypted group creation ([#6927](https://github.com/chatmail/core/pull/6927)).
|
||||
|
||||
## [2.1.0] - 2025-07-11
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add account ordering functionality ([#6993](https://github.com/chatmail/core/pull/6993)).
|
||||
- feat: Make it possible to leave broadcast channels ([#6984](https://github.com/chatmail/core/pull/6984))
|
||||
- Migrations: Use tools::Time to measure time for logging.
|
||||
- Log emitted logging events with `tracing`.
|
||||
- Ensure_and_debug_assert{,_eq,_ne} macros combining `debug_assert*` and anyhow::ensure ([#6907](https://github.com/chatmail/core/pull/6907)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Use Viewtype::File for messages with invalid images, images of unknown size, images > 50 Mpx ([#6825](https://github.com/chatmail/core/pull/6825)).
|
||||
- Don't apply chat name and avatar changes from non-members.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update showpadlock ffi.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Update cordyceps from 0.3.2 to 0.3.4.
|
||||
|
||||
### Tests
|
||||
|
||||
- Add option to save database on test failure ([#6992](https://github.com/chatmail/core/pull/6992)).
|
||||
|
||||
## [2.0.0] - 2025-07-09
|
||||
|
||||
This release changes the way the core handles contact keys.
|
||||
@@ -1391,7 +1866,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91..
|
||||
### Fixes
|
||||
|
||||
- Reset quota on configured address change ([#5908](https://github.com/chatmail/core/pull/5908)).
|
||||
- Do not emit progress 1000 when configuration is cancelled.
|
||||
- Do not emit progress 1000 when configuration is canceled.
|
||||
- Assume file extensions are 32 chars max and don't contain whitespace ([#5338](https://github.com/chatmail/core/pull/5338)).
|
||||
- Re-add tokens.foreign_id column ([#6038](https://github.com/chatmail/core/pull/6038)).
|
||||
|
||||
@@ -3839,7 +4314,7 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/chatma
|
||||
- Recreate `smtp` table with AUTOINCREMENT `id` ([#4390](https://github.com/chatmail/core/pull/4390)).
|
||||
- Do not return an error from `send_msg_to_smtp` if retry limit is exceeded.
|
||||
- Make the bots automatically accept group chat contact requests ([#4377](https://github.com/chatmail/core/pull/4377)).
|
||||
- Delete `smtp` rows when message sending is cancelled ([#4391](https://github.com/chatmail/core/pull/4391)).
|
||||
- Delete `smtp` rows when message sending is canceled ([#4391](https://github.com/chatmail/core/pull/4391)).
|
||||
|
||||
### Refactor
|
||||
|
||||
@@ -3850,7 +4325,7 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/chatma
|
||||
### Fixes
|
||||
|
||||
- Fetch at most 100 existing messages even if EXISTS was not received.
|
||||
- Delete `smtp` rows when message sending is cancelled.
|
||||
- Delete `smtp` rows when message sending is canceled.
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -3937,14 +4412,14 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/chatma
|
||||
## [1.112.3] - 2023-03-30
|
||||
|
||||
### Fixes
|
||||
- `transfer::get_backup` now frees ongoing process when cancelled. #4249
|
||||
- `transfer::get_backup` now frees ongoing process when canceled. #4249
|
||||
|
||||
## [1.112.2] - 2023-03-30
|
||||
|
||||
### Changes
|
||||
- Update iroh, remove `default-net` from `[patch.crates-io]` section.
|
||||
- transfer backup: Connect to multiple provider addresses concurrently. This should speed up connection time significantly on the getter side. #4240
|
||||
- Make sure BackupProvider is cancelled on drop (or `dc_backup_provider_unref`). The BackupProvider will now always finish with an IMEX event of 1000 or 0, previously it would sometimes finished with 1000 (success) when it really was 0 (failure). #4242
|
||||
- Make sure BackupProvider is canceled on drop (or `dc_backup_provider_unref`). The BackupProvider will now always finish with an IMEX event of 1000 or 0, previously it would sometimes finished with 1000 (success) when it really was 0 (failure). #4242
|
||||
|
||||
### Fixes
|
||||
- Do not return media from trashed messages in the "All media" view. #4247
|
||||
@@ -6426,3 +6901,23 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.159.5]: https://github.com/chatmail/core/compare/v1.159.4..v1.159.5
|
||||
[1.160.0]: https://github.com/chatmail/core/compare/v1.159.5..v1.160.0
|
||||
[2.0.0]: https://github.com/chatmail/core/compare/v1.160.0..v2.0.0
|
||||
[2.1.0]: https://github.com/chatmail/core/compare/v2.0.0..v2.1.0
|
||||
[2.2.0]: https://github.com/chatmail/core/compare/v2.1.0..v2.2.0
|
||||
[2.3.0]: https://github.com/chatmail/core/compare/v2.2.0..v2.3.0
|
||||
[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
|
||||
[2.13.0]: https://github.com/chatmail/core/compare/v2.12.0..v2.13.0
|
||||
[2.14.0]: https://github.com/chatmail/core/compare/v2.13.0..v2.14.0
|
||||
[2.15.0]: https://github.com/chatmail/core/compare/v2.14.0..v2.15.0
|
||||
[2.16.0]: https://github.com/chatmail/core/compare/v2.15.0..v2.16.0
|
||||
[2.17.0]: https://github.com/chatmail/core/compare/v2.16.0..v2.17.0
|
||||
[2.18.0]: https://github.com/chatmail/core/compare/v2.17.0..v2.18.0
|
||||
[2.19.0]: https://github.com/chatmail/core/compare/v2.18.0..v2.19.0
|
||||
[2.20.0]: https://github.com/chatmail/core/compare/v2.19.0..v2.20.0
|
||||
|
||||
@@ -44,7 +44,7 @@ If you want to contribute a code, follow this guide.
|
||||
|
||||
The following prefix types are used:
|
||||
- `feat`: Features, e.g. "feat: Pause IO for BackupProvider". If you are unsure what's the category of your commit, you can often just use `feat`.
|
||||
- `fix`: Bug fixes, e.g. "fix: delete `smtp` rows when message sending is cancelled"
|
||||
- `fix`: Bug fixes, e.g. "fix: delete `smtp` rows when message sending is canceled"
|
||||
- `api`: API changes, e.g. "api(rust): add `get_msg_read_receipts(context, msg_id)`"
|
||||
- `refactor`: Refactorings, e.g. "refactor: iterate over `msg_ids` without `.iter()`"
|
||||
- `perf`: Performance improvements, e.g. "perf: improve SQLite performance with `PRAGMA synchronous=normal`"
|
||||
|
||||
612
Cargo.lock
generated
612
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
36
Cargo.toml
36
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "2.0.0"
|
||||
version = "2.20.0"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.85"
|
||||
@@ -44,14 +44,16 @@ ratelimit = { path = "./deltachat-ratelimit" }
|
||||
anyhow = { workspace = true }
|
||||
async-broadcast = "0.7.2"
|
||||
async-channel = { workspace = true }
|
||||
async-imap = { version = "0.10.4", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||
async-imap = { version = "0.11.1", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
|
||||
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
|
||||
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"
|
||||
@@ -63,13 +65,13 @@ hickory-resolver = "0.25.2"
|
||||
http-body-util = "0.1.3"
|
||||
humansize = "2"
|
||||
hyper = "1"
|
||||
hyper-util = "0.1.14"
|
||||
hyper-util = "0.1.16"
|
||||
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.3", default-features = false }
|
||||
mail-builder = { version = "0.4.4", default-features = false }
|
||||
mailparse = { workspace = true }
|
||||
mime = "0.3.17"
|
||||
num_cpus = "1.17"
|
||||
@@ -77,18 +79,17 @@ num-derive = "0.4"
|
||||
num-traits = { workspace = true }
|
||||
parking_lot = "0.12.4"
|
||||
percent-encoding = "2.3"
|
||||
pgp = { version = "0.16.0", default-features = false }
|
||||
pgp = { version = "0.17.0", default-features = false }
|
||||
pin-project = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = "0.37"
|
||||
quoted_printable = "0.5"
|
||||
quick-xml = { version = "0.38", features = ["escape-html"] }
|
||||
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 }
|
||||
sdp = "0.8.0"
|
||||
serde_json = { workspace = true }
|
||||
serde_urlencoded = "0.7.1"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
@@ -101,22 +102,21 @@ strum_macros = "0.27"
|
||||
tagger = "4.3.4"
|
||||
textwrap = "0.16.2"
|
||||
thiserror = { workspace = true }
|
||||
tokio-io-timeout = "1.2.0"
|
||||
tokio-io-timeout = "1.2.1"
|
||||
tokio-rustls = { version = "0.26.2", default-features = false }
|
||||
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.8"
|
||||
toml = "0.9"
|
||||
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.6.0", features = ["async_tokio"] }
|
||||
criterion = { version = "0.7.0", features = ["async_tokio"] }
|
||||
futures-lite = { workspace = true }
|
||||
log = { workspace = true }
|
||||
nu-ansi-term = { workspace = true }
|
||||
@@ -175,18 +175,18 @@ harness = false
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1"
|
||||
async-channel = "2.3.1"
|
||||
async-channel = "2.5.0"
|
||||
base64 = "0.22"
|
||||
chrono = { version = "0.4.41", default-features = false }
|
||||
chrono = { version = "0.4.42", default-features = false }
|
||||
deltachat-contact-tools = { path = "deltachat-contact-tools" }
|
||||
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
|
||||
deltachat = { path = ".", default-features = false }
|
||||
futures = "0.3.31"
|
||||
futures-lite = "2.6.0"
|
||||
futures-lite = "2.6.1"
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
mailparse = "0.16.1"
|
||||
nu-ansi-term = "0.46"
|
||||
nu-ansi-term = "0.50"
|
||||
num-traits = "0.2"
|
||||
rand = "0.8"
|
||||
regex = "1.10"
|
||||
@@ -194,10 +194,10 @@ rusqlite = "0.36"
|
||||
sanitize-filename = "0.5"
|
||||
serde = "1.0"
|
||||
serde_json = "1"
|
||||
tempfile = "3.20.0"
|
||||
tempfile = "3.23.0"
|
||||
thiserror = "2"
|
||||
tokio = "1"
|
||||
tokio-util = "0.7.14"
|
||||
tokio-util = "0.7.16"
|
||||
tracing-subscriber = "0.3"
|
||||
yerpc = "0.6.4"
|
||||
|
||||
|
||||
27
README.md
27
README.md
@@ -80,30 +80,41 @@ Connect to your mail server (if already configured):
|
||||
> connect
|
||||
```
|
||||
|
||||
Create a contact:
|
||||
Export your public key to a vCard file:
|
||||
|
||||
```
|
||||
> make-vcard my.vcard 1
|
||||
```
|
||||
|
||||
Create contacts by address or vCard file:
|
||||
|
||||
```
|
||||
> addcontact yourfriends@email.org
|
||||
Command executed successfully.
|
||||
> import-vcard key-contact.vcard
|
||||
```
|
||||
|
||||
List contacts:
|
||||
|
||||
```
|
||||
> listcontacts
|
||||
Contact#10: <name unset> <yourfriends@email.org>
|
||||
Contact#1: Me √√ <your@email.org>
|
||||
Contact#Contact#11: key-contact@email.org <key-contact@email.org>
|
||||
Contact#Contact#Self: Me √ <your@email.org>
|
||||
2 key contacts.
|
||||
Contact#Contact#10: yourfriends@email.org <yourfriends@email.org>
|
||||
1 address contacts.
|
||||
```
|
||||
|
||||
Create a chat with your friend and send a message:
|
||||
|
||||
```
|
||||
> createchat 10
|
||||
Single#10 created successfully.
|
||||
> chat 10
|
||||
Single#10: yourfriends@email.org [yourfriends@email.org]
|
||||
Single#Chat#12 created successfully.
|
||||
> chat 12
|
||||
Selecting chat Chat#12
|
||||
Single#Chat#12: yourfriends@email.org [yourfriends@email.org] Icon: profile-db-blobs/4138c52e5bc1c576cda7dd44d088c07.png
|
||||
0 messages.
|
||||
81.252µs to create this list, 123.625µs to mark all messages as noticed.
|
||||
> send hi
|
||||
Message sent.
|
||||
```
|
||||
|
||||
List messages when inside a chat:
|
||||
|
||||
33
STYLE.md
33
STYLE.md
@@ -78,6 +78,27 @@ All errors should be handled in one of these ways:
|
||||
- With `.log_err().ok()`.
|
||||
- Bubbled up with `?`.
|
||||
|
||||
When working with [async streams](https://docs.rs/futures/0.3.31/futures/stream/index.html),
|
||||
prefer [`try_next`](https://docs.rs/futures/0.3.31/futures/stream/trait.TryStreamExt.html#method.try_next) over `next()`, e.g. do
|
||||
```
|
||||
while let Some(event) = stream.try_next().await? {
|
||||
todo!();
|
||||
}
|
||||
```
|
||||
instead of
|
||||
```
|
||||
while let Some(event_res) = stream.next().await {
|
||||
todo!();
|
||||
}
|
||||
```
|
||||
as it allows bubbling up the error early with `?`
|
||||
with no way to accidentally skip error processing
|
||||
with early `continue` or `break`.
|
||||
Some streams reading from a connection
|
||||
return infinite number of `Some(Err(_))`
|
||||
items when connection breaks and not processing
|
||||
errors may result in infinite loop.
|
||||
|
||||
`backtrace` feature is enabled for `anyhow` crate
|
||||
and `debug = 1` option is set in the test profile.
|
||||
This allows to run `RUST_BACKTRACE=1 cargo test`
|
||||
@@ -91,6 +112,18 @@ Follow
|
||||
<https://doc.rust-lang.org/core/error/index.html#common-message-styles>
|
||||
for `.expect` message style.
|
||||
|
||||
## BTreeMap vs HashMap
|
||||
|
||||
Prefer [BTreeMap](https://doc.rust-lang.org/std/collections/struct.BTreeMap.html)
|
||||
over [HashMap](https://doc.rust-lang.org/std/collections/struct.HashMap.html)
|
||||
and [BTreeSet](https://doc.rust-lang.org/std/collections/struct.BTreeSet.html)
|
||||
over [HashSet](https://doc.rust-lang.org/std/collections/struct.HashSet.html)
|
||||
as iterating over these structures returns items in deterministic order.
|
||||
|
||||
Non-deterministic code may result in difficult to reproduce bugs,
|
||||
flaky tests, regression tests that miss bugs
|
||||
or different behavior on different devices when processing the same messages.
|
||||
|
||||
## Logging
|
||||
|
||||
For logging, use `info!`, `warn!` and `error!` macros.
|
||||
|
||||
|
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 |
@@ -68,7 +68,7 @@ impl ContactAddress {
|
||||
pub fn new(s: &str) -> Result<Self> {
|
||||
let addr = addr_normalize(s);
|
||||
if !may_be_valid_addr(&addr) {
|
||||
bail!("invalid address {:?}", s);
|
||||
bail!("invalid address {s:?}");
|
||||
}
|
||||
Ok(Self(addr.to_string()))
|
||||
}
|
||||
@@ -257,16 +257,16 @@ impl EmailAddress {
|
||||
.chars()
|
||||
.any(|c| c.is_whitespace() || c == '<' || c == '>')
|
||||
{
|
||||
bail!("Email {:?} must not contain whitespaces, '>' or '<'", input);
|
||||
bail!("Email {input:?} must not contain whitespaces, '>' or '<'");
|
||||
}
|
||||
|
||||
match &parts[..] {
|
||||
[domain, local] => {
|
||||
if local.is_empty() {
|
||||
bail!("empty string is not valid for local part in {:?}", input);
|
||||
bail!("empty string is not valid for local part in {input:?}");
|
||||
}
|
||||
if domain.is_empty() {
|
||||
bail!("missing domain after '@' in {:?}", input);
|
||||
bail!("missing domain after '@' in {input:?}");
|
||||
}
|
||||
if domain.ends_with('.') {
|
||||
bail!("Domain {domain:?} should not contain the dot in the end");
|
||||
@@ -276,7 +276,7 @@ impl EmailAddress {
|
||||
domain: (*domain).to_string(),
|
||||
})
|
||||
}
|
||||
_ => bail!("Email {:?} must contain '@' character", input),
|
||||
_ => bail!("Email {input:?} must contain '@' character"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.0.0"
|
||||
version = "2.20.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -415,7 +415,6 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* As for `displayname` and `selfstatus`, also the avatar is sent to the recipients.
|
||||
* To save traffic, however, the avatar is attached only as needed
|
||||
* and also recoded to a reasonable size.
|
||||
* - `e2ee_enabled` = 0=no end-to-end-encryption, 1=prefer end-to-end-encryption (default)
|
||||
* - `mdns_enabled` = 0=do not send or request read receipts,
|
||||
* 1=send and request read receipts
|
||||
* default=send and request read receipts, only send but not request if `bot` is set
|
||||
@@ -459,12 +458,6 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* The library uses the `media_quality` setting to use different defaults
|
||||
* for recoding images sent with type #DC_MSG_IMAGE.
|
||||
* If needed, recoding other file types is up to the UI.
|
||||
* - `webrtc_instance` = webrtc instance to use for videochats in the form
|
||||
* `[basicwebrtc:|jitsi:]https://example.com/subdir#roomname=$ROOM`
|
||||
* if the URL is prefixed by `basicwebrtc`, the server is assumed to be of the type
|
||||
* https://github.com/cracker0dks/basicwebrtc which some UIs have native support for.
|
||||
* The type `jitsi:` may be handled by external apps.
|
||||
* If no type is prefixed, the videochat is handled completely in a browser.
|
||||
* - `bot` = Set to "1" if this is a bot.
|
||||
* Prevents adding the "Device messages" and "Saved messages" chats,
|
||||
* adds Auto-Submitted header to outgoing messages,
|
||||
@@ -503,13 +496,6 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in
|
||||
* seconds. 2 days by default.
|
||||
* This is not supposed to be changed by UIs and only used for testing.
|
||||
* - `verified_one_on_one_chats` = Feature flag for verified 1:1 chats; the UI should set it
|
||||
* to 1 if it supports verified 1:1 chats.
|
||||
* Regardless of this setting, `dc_chat_is_protected()` returns true while the key is verified,
|
||||
* and when the key changes, an info message is posted into the chat.
|
||||
* 0=Nothing else happens when the key changes.
|
||||
* 1=After the key changed, `dc_chat_can_send()` returns false and `dc_chat_is_protection_broken()` returns true
|
||||
* until `dc_accept_chat()` is called.
|
||||
* - `is_chatmail` = 1 if the the server is a chatmail server, 0 otherwise.
|
||||
* - `is_muted` = Whether a context is muted by the user.
|
||||
* Muted contexts should not sound, vibrate or show notifications.
|
||||
@@ -583,11 +569,10 @@ int dc_set_stock_translation(dc_context_t* context, uint32_t stock_i
|
||||
/**
|
||||
* Set configuration values from a QR code.
|
||||
* Before this function is called, dc_check_qr() should confirm the type of the
|
||||
* QR code is DC_QR_ACCOUNT, DC_QR_LOGIN or DC_QR_WEBRTC_INSTANCE.
|
||||
* QR code is DC_QR_ACCOUNT or DC_QR_LOGIN.
|
||||
*
|
||||
* Internally, the function will call dc_set_config() with the appropriate keys,
|
||||
* e.g. `addr` and `mail_pw` for DC_QR_ACCOUNT and DC_QR_LOGIN
|
||||
* or `webrtc_instance` for DC_QR_WEBRTC_INSTANCE.
|
||||
* e.g. `addr` and `mail_pw` for DC_QR_ACCOUNT and DC_QR_LOGIN.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
@@ -1060,42 +1045,6 @@ void dc_send_edit_request (dc_context_t* context, uint32_t ms
|
||||
void dc_send_delete_request (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt);
|
||||
|
||||
|
||||
/**
|
||||
* Send invitation to a videochat.
|
||||
*
|
||||
* This function reads the `webrtc_instance` config value,
|
||||
* may check that the server is working in some way
|
||||
* and creates a unique room for this chat, if needed doing a TOKEN roundtrip for that.
|
||||
*
|
||||
* After that, the function sends out a message that contains information to join the room:
|
||||
*
|
||||
* - To allow non-delta-clients to join the chat,
|
||||
* the message contains a text-area with some descriptive text
|
||||
* and a URL that can be opened in a supported browser to join the videochat.
|
||||
*
|
||||
* - delta-clients can get all information needed from
|
||||
* the message object, using e.g.
|
||||
* dc_msg_get_videochat_url() and check dc_msg_get_viewtype() for #DC_MSG_VIDEOCHAT_INVITATION.
|
||||
*
|
||||
* dc_send_videochat_invitation() is blocking and may take a while,
|
||||
* so the UIs will typically call the function from within a thread.
|
||||
* Moreover, UIs will typically enter the room directly without an additional click on the message,
|
||||
* for this purpose, the function returns the message id directly.
|
||||
*
|
||||
* As for other messages sent, this function
|
||||
* sends the event #DC_EVENT_MSGS_CHANGED on success, the message has a delivery state, and so on.
|
||||
* The recipient will get noticed by the call as usual by #DC_EVENT_INCOMING_MSG or #DC_EVENT_MSGS_CHANGED,
|
||||
* However, UIs might some things differently, e.g. play a different sound.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param chat_id The chat to start a videochat for.
|
||||
* @return The ID of the message sent out
|
||||
* or 0 for errors.
|
||||
*/
|
||||
uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* A webxdc instance sends a status update to its other members.
|
||||
*
|
||||
@@ -1222,6 +1171,117 @@ void dc_set_webxdc_integration (dc_context_t* context, const char* f
|
||||
uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* Start an outgoing call.
|
||||
* This sends a message of type #DC_MSG_CALL with all relevant information to the callee,
|
||||
* who will get informed by an #DC_EVENT_INCOMING_CALL event and rings.
|
||||
*
|
||||
* Possible actions during ringing:
|
||||
*
|
||||
* - caller cancels the call using dc_end_call():
|
||||
* callee receives #DC_EVENT_CALL_ENDED and has a "Missed call"
|
||||
*
|
||||
* - callee accepts using dc_accept_incoming_call():
|
||||
* caller receives #DC_EVENT_OUTGOING_CALL_ACCEPTED.
|
||||
* callee's devices receive #DC_EVENT_INCOMING_CALL_ACCEPTED, call starts
|
||||
*
|
||||
* - callee declines using dc_end_call():
|
||||
* caller receives #DC_EVENT_CALL_ENDED and has a "Declinced Call".
|
||||
* callee's other devices receive #DC_EVENT_CALL_ENDED and have a "Canceled Call",
|
||||
*
|
||||
* - callee is already in a call:
|
||||
* what to do depends on the capabilities of UI to handle calls.
|
||||
* if UI cannot handle multiple calls, an easy approach would be to decline the new call automatically
|
||||
* and make that visble to the user in the call, e.g. by a notification
|
||||
*
|
||||
* - timeout:
|
||||
* after 1 minute without action,
|
||||
* caller and callee receive #DC_EVENT_CALL_ENDED
|
||||
* to prevent endless ringing of callee
|
||||
* in case caller got offline without being able to send cancellation message.
|
||||
* for caller, this is a "Canceled call";
|
||||
* for callee, this is a "Missed call"
|
||||
*
|
||||
* Actions during the call:
|
||||
*
|
||||
* - caller ends the call using dc_end_call():
|
||||
* callee receives #DC_EVENT_CALL_ENDED
|
||||
*
|
||||
* - callee ends the call using dc_end_call():
|
||||
* caller receives #DC_EVENT_CALL_ENDED
|
||||
*
|
||||
* Contact request handling:
|
||||
*
|
||||
* - placing or accepting calls implies accepting contact requests
|
||||
*
|
||||
* - ending a call does not accept a contact request;
|
||||
* instead, the call will timeout on all affected devices.
|
||||
*
|
||||
* Note, that the events are for updating the call screen,
|
||||
* possible status messages are added and updated as usual, including the known events.
|
||||
* In the UI, the sorted chatlist is used as an overview about calls as well as messages.
|
||||
* To place a call with a contact that has no chat yet, use dc_create_chat_by_contact_id() first.
|
||||
*
|
||||
* UI will usually allow only one call at the same time,
|
||||
* this has to be tracked by UI across profile, the core does not track this.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param chat_id The chat to place a call for.
|
||||
* This needs to be a one-to-one chat.
|
||||
* @param place_call_info any data that other devices receive
|
||||
* in #DC_EVENT_INCOMING_CALL.
|
||||
* @return ID of the system message announcing the call.
|
||||
*/
|
||||
uint32_t dc_place_outgoing_call (dc_context_t* context, uint32_t chat_id, const char* place_call_info);
|
||||
|
||||
|
||||
/**
|
||||
* Accept incoming call.
|
||||
*
|
||||
* This implicitly accepts the contact request, if not yet done.
|
||||
* All affected devices will receive
|
||||
* either #DC_EVENT_OUTGOING_CALL_ACCEPTED or #DC_EVENT_INCOMING_CALL_ACCEPTED.
|
||||
*
|
||||
* If the call is already accepted or ended, nothing happens.
|
||||
* If the chat is a contact request, it is accepted implicitly.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param msg_id The ID of the call to accept.
|
||||
* This is the ID reported by #DC_EVENT_INCOMING_CALL
|
||||
* and equals to the ID of the corresponding info message.
|
||||
* @param accept_call_info any data that other devices receive
|
||||
* in #DC_EVENT_OUTGOING_CALL_ACCEPTED.
|
||||
* @return 1=success, 0=error
|
||||
*/
|
||||
int dc_accept_incoming_call (dc_context_t* context, uint32_t msg_id, const char* accept_call_info);
|
||||
|
||||
|
||||
/**
|
||||
* End incoming or outgoing call.
|
||||
*
|
||||
* For unaccepted calls ended by the caller, this is a "cancellation".
|
||||
* Unaccepted calls ended by the callee are a "decline".
|
||||
* If the call was accepted, this is a "hangup".
|
||||
*
|
||||
* All participant devices get informed about the ended call via #DC_EVENT_CALL_ENDED unless they are contact requests.
|
||||
* For contact requests, the call times out on all other affected devices.
|
||||
*
|
||||
* If the message ID is wrong or does not exist for whatever reasons, nothing happens.
|
||||
* Therefore, and for resilience, UI should remove the call UI directly when calling
|
||||
* this function and not only on the event.
|
||||
*
|
||||
* If the call is already ended, nothing happens.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param msg_id the ID of the call.
|
||||
* @return 1=success, 0=error
|
||||
*/
|
||||
int dc_end_call (dc_context_t* context, uint32_t msg_id);
|
||||
|
||||
|
||||
/**
|
||||
* Save a draft for a chat in the database.
|
||||
*
|
||||
@@ -1339,12 +1399,14 @@ 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.
|
||||
* To get the concrete time of the marker, use dc_array_get_timestamp().
|
||||
* The day marker timestamp is the midnight one for the corresponding (following) day in the local timezone.
|
||||
* 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.
|
||||
@@ -2094,9 +2156,19 @@ int dc_may_be_valid_addr (const char* addr);
|
||||
|
||||
|
||||
/**
|
||||
* Check if an e-mail address belongs to a known and unblocked 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 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().
|
||||
*
|
||||
@@ -2118,6 +2190,13 @@ 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
|
||||
@@ -2493,7 +2572,6 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
#define DC_QR_BACKUP 251 // deprecated
|
||||
#define DC_QR_BACKUP2 252
|
||||
#define DC_QR_BACKUP_TOO_NEW 255
|
||||
#define DC_QR_WEBRTC_INSTANCE 260 // text1=domain, text2=instance pattern
|
||||
#define DC_QR_PROXY 271 // text1=address (e.g. "127.0.0.1:9050")
|
||||
#define DC_QR_ADDR 320 // id=contact
|
||||
#define DC_QR_TEXT 330 // text1=text
|
||||
@@ -2547,10 +2625,6 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
* show a hint to the user that this backup comes from a newer Delta Chat version
|
||||
* and this device needs an update
|
||||
*
|
||||
* - DC_QR_WEBRTC_INSTANCE with dc_lot_t::text1=domain:
|
||||
* ask the user if they want to use the given service for video chats;
|
||||
* if so, call dc_set_config_from_qr().
|
||||
*
|
||||
* - DC_QR_PROXY with dc_lot_t::text1=address:
|
||||
* ask the user if they want to use the given proxy.
|
||||
* if so, call dc_set_config_from_qr() and restart I/O.
|
||||
@@ -3818,21 +3892,12 @@ int dc_chat_can_send (const dc_chat_t* chat);
|
||||
/**
|
||||
* Check if a chat is protected.
|
||||
*
|
||||
* End-to-end encryption is guaranteed in protected chats
|
||||
* and only verified contacts
|
||||
* Only verified contacts
|
||||
* as determined by dc_contact_is_verified()
|
||||
* can be added to protected chats.
|
||||
*
|
||||
* Protected chats are created using dc_create_group_chat()
|
||||
* by setting the 'protect' parameter to 1.
|
||||
* 1:1 chats become protected or unprotected automatically
|
||||
* if `verified_one_on_one_chats` setting is enabled.
|
||||
*
|
||||
* UI should display a green checkmark
|
||||
* in the chat title,
|
||||
* in the chatlist item
|
||||
* and in the chat profile
|
||||
* if chat protection is enabled.
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
* @param chat The chat object.
|
||||
@@ -3856,26 +3921,6 @@ int dc_chat_is_protected (const dc_chat_t* chat);
|
||||
int dc_chat_is_encrypted (const dc_chat_t *chat);
|
||||
|
||||
|
||||
/**
|
||||
* Checks 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.
|
||||
* dc_chat_is_protection_broken() will return true until dc_accept_chat() 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 dc_accept_chat().
|
||||
* @memberof dc_chat_t
|
||||
* @param chat The chat object.
|
||||
* @return 1=chat protection broken, 0=otherwise.
|
||||
*/
|
||||
int dc_chat_is_protection_broken (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Check if locations are sent to the chat
|
||||
* at the time the object was created using dc_get_chat().
|
||||
@@ -4303,11 +4348,16 @@ int dc_msg_get_duration (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Check if a padlock should be shown beside the message.
|
||||
* Check if message was correctly encrypted and signed.
|
||||
*
|
||||
* Historically, UIs showed a small padlock on the message then.
|
||||
* Today, the UIs should instead
|
||||
* show a small email-icon on the message if the message is not encrypted or signed,
|
||||
* and nothing otherwise.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return 1=padlock should be shown beside message, 0=do not show a padlock beside the message.
|
||||
* @return 1=message correctly encrypted and signed, no need to show anything; 0=show email-icon beside the message.
|
||||
*/
|
||||
int dc_msg_get_showpadlock (const dc_msg_t* msg);
|
||||
|
||||
@@ -4530,12 +4580,12 @@ int dc_msg_is_info (const dc_msg_t* msg);
|
||||
* - DC_INFO_MEMBER_ADDED_TO_GROUP (4) - "Member CONTACT added by OTHER_CONTACT"
|
||||
* - DC_INFO_MEMBER_REMOVED_FROM_GROUP (5) - "Member CONTACT removed by OTHER_CONTACT"
|
||||
* - DC_INFO_EPHEMERAL_TIMER_CHANGED (10) - "Disappearing messages CHANGED_TO by CONTACT"
|
||||
* - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is now protected"
|
||||
* - DC_INFO_PROTECTION_DISABLED (12) - Info-message for "Chat is no longer protected"
|
||||
* - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is protected"
|
||||
* - DC_INFO_INVALID_UNENCRYPTED_MAIL (13) - Info-message for "Provider requires end-to-end encryption which is not setup yet",
|
||||
* the UI should change the corresponding string using #DC_STR_INVALID_UNENCRYPTED_MAIL
|
||||
* and also offer a way to fix the encryption, eg. by a button offering a QR scan
|
||||
* - DC_INFO_WEBXDC_INFO_MESSAGE (32) - Info-message created by webxdc app sending `update.info`
|
||||
* - DC_INFO_CHAT_E2EE (50) - Info-message for "Chat is end-to-end-encrypted"
|
||||
*
|
||||
* For the messages that refer to a CONTACT,
|
||||
* dc_msg_get_info_contact_id() returns the contact ID.
|
||||
@@ -4588,9 +4638,10 @@ uint32_t dc_msg_get_info_contact_id (const dc_msg_t* msg);
|
||||
#define DC_INFO_LOCATION_ONLY 9
|
||||
#define DC_INFO_EPHEMERAL_TIMER_CHANGED 10
|
||||
#define DC_INFO_PROTECTION_ENABLED 11
|
||||
#define DC_INFO_PROTECTION_DISABLED 12
|
||||
#define DC_INFO_PROTECTION_DISABLED 12 // deprecated 2025-07
|
||||
#define DC_INFO_INVALID_UNENCRYPTED_MAIL 13
|
||||
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
|
||||
#define DC_INFO_CHAT_E2EE 50
|
||||
|
||||
|
||||
/**
|
||||
@@ -4645,22 +4696,6 @@ int dc_msg_is_setupmessage (const dc_msg_t* msg);
|
||||
char* dc_msg_get_setupcodebegin (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Get URL of a videochat invitation.
|
||||
*
|
||||
* Videochat invitations are sent out using dc_send_videochat_invitation()
|
||||
* and dc_msg_get_viewtype() returns #DC_MSG_VIDEOCHAT_INVITATION for such invitations.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return If the message contains a videochat invitation,
|
||||
* the URL of the invitation is returned.
|
||||
* If the message is no videochat invitation, NULL is returned.
|
||||
* Must be released using dc_str_unref() when done.
|
||||
*/
|
||||
char* dc_msg_get_videochat_url (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Gets the error status of the message.
|
||||
* If there is no error associated with the message, NULL is returned.
|
||||
@@ -4683,41 +4718,6 @@ char* dc_msg_get_videochat_url (const dc_msg_t* msg);
|
||||
char* dc_msg_get_error (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Get type of videochat.
|
||||
*
|
||||
* Calling this functions only makes sense for messages of type #DC_MSG_VIDEOCHAT_INVITATION,
|
||||
* in this case, if `basicwebrtc:` as of https://github.com/cracker0dks/basicwebrtc or `jitsi`
|
||||
* were used to initiate the videochat,
|
||||
* dc_msg_get_videochat_type() returns the corresponding type.
|
||||
*
|
||||
* The videochat URL can be retrieved using dc_msg_get_videochat_url().
|
||||
* To check if a message is a videochat invitation at all, check the message type for #DC_MSG_VIDEOCHAT_INVITATION.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return Type of the videochat as of DC_VIDEOCHATTYPE_BASICWEBRTC, DC_VIDEOCHATTYPE_JITSI or DC_VIDEOCHATTYPE_UNKNOWN.
|
||||
*
|
||||
* Example:
|
||||
* ~~~
|
||||
* if (dc_msg_get_viewtype(msg) == DC_MSG_VIDEOCHAT_INVITATION) {
|
||||
* if (dc_msg_get_videochat_type(msg) == DC_VIDEOCHATTYPE_BASICWEBRTC) {
|
||||
* // videochat invitation that we ship a client for
|
||||
* } else {
|
||||
* // use browser for videochat - or add an additional check for DC_VIDEOCHATTYPE_JITSI
|
||||
* }
|
||||
* } else {
|
||||
* // not a videochat invitation
|
||||
* }
|
||||
* ~~~
|
||||
*/
|
||||
int dc_msg_get_videochat_type (const dc_msg_t* msg);
|
||||
|
||||
#define DC_VIDEOCHATTYPE_UNKNOWN 0
|
||||
#define DC_VIDEOCHATTYPE_BASICWEBRTC 1
|
||||
#define DC_VIDEOCHATTYPE_JITSI 2
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the message has a full HTML version.
|
||||
*
|
||||
@@ -5261,20 +5261,14 @@ int dc_contact_is_blocked (const dc_contact_t* contact);
|
||||
|
||||
/**
|
||||
* Check if the contact
|
||||
* can be added to verified chats,
|
||||
* i.e. has a verified key
|
||||
* and Autocrypt key matches the verified key.
|
||||
* can be added to protected chats.
|
||||
*
|
||||
* If contact is verified
|
||||
* UI should display green checkmark after the contact name
|
||||
* in contact list items,
|
||||
* in chat member list items
|
||||
* and in profiles if no chat with the contact exist (otherwise, use dc_chat_is_protected()).
|
||||
* See dc_contact_get_verifier_id() for a guidance how to display these information.
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
* @return 0: contact is not verified.
|
||||
* 2: SELF and contact have verified their fingerprints in both directions; in the UI typically checkmarks are shown.
|
||||
* 2: SELF and contact have verified their fingerprints in both directions.
|
||||
*/
|
||||
int dc_contact_is_verified (dc_contact_t* contact);
|
||||
|
||||
@@ -5305,16 +5299,22 @@ int dc_contact_is_key_contact (dc_contact_t* contact);
|
||||
/**
|
||||
* Return the contact ID that verified a contact.
|
||||
*
|
||||
* If the function returns non-zero result,
|
||||
* display green checkmark in the profile and "Introduced by ..." line
|
||||
* with the name and address of the contact
|
||||
* formatted by dc_contact_get_name_n_addr.
|
||||
* As verifier may be unknown,
|
||||
* use dc_contact_is_verified() to check if a contact can be added to a protected chat.
|
||||
*
|
||||
* If this function returns a verifier,
|
||||
* this does not necessarily mean
|
||||
* you can add the contact to verified chats.
|
||||
* Use dc_contact_is_verified() to check
|
||||
* if a contact can be added to a verified chat instead.
|
||||
* UI should display the information in the contact's profile as follows:
|
||||
*
|
||||
* - If dc_contact_get_verifier_id() != 0,
|
||||
* display text "Introduced by ..."
|
||||
* with the name and address of the contact
|
||||
* formatted by dc_contact_get_name_n_addr().
|
||||
* Prefix the text by a green checkmark.
|
||||
*
|
||||
* - If dc_contact_get_verifier_id() == 0 and dc_contact_is_verified() != 0,
|
||||
* display "Introduced" prefixed by a green checkmark.
|
||||
*
|
||||
* - if dc_contact_get_verifier_id() == 0 and dc_contact_is_verified() == 0,
|
||||
* display nothing
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
@@ -5617,14 +5617,21 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
|
||||
|
||||
/**
|
||||
* Message indicating an incoming or outgoing videochat.
|
||||
* The message was created via dc_send_videochat_invitation() on this or a remote device.
|
||||
* Message indicating an incoming or outgoing call.
|
||||
*
|
||||
* Typically, such messages are rendered differently by the UIs,
|
||||
* e.g. contain a button to join the videochat.
|
||||
* The URL for joining can be retrieved using dc_msg_get_videochat_url().
|
||||
* These messages are created by dc_place_outgoing_call()
|
||||
* and should be rendered by UI similar to text messages,
|
||||
* maybe with some "phone icon" at the side.
|
||||
*
|
||||
* The message text is updated as needed
|
||||
* and UI will be informed via #DC_EVENT_MSGS_CHANGED as usual.
|
||||
*
|
||||
* Do not start ringing when seeing this message;
|
||||
* the mesage may belong e.g. to an old missed call.
|
||||
*
|
||||
* Instead, ringing should start on the event #DC_EVENT_INCOMING_CALL
|
||||
*/
|
||||
#define DC_MSG_VIDEOCHAT_INVITATION 70
|
||||
#define DC_MSG_CALL 71
|
||||
|
||||
|
||||
/**
|
||||
@@ -6380,7 +6387,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
/**
|
||||
* Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
||||
* Or the verify state of a chat has changed.
|
||||
* See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
|
||||
* and dc_remove_contact_from_chat().
|
||||
*
|
||||
@@ -6470,11 +6476,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
* generated by dc_get_securejoin_qr().
|
||||
*
|
||||
* @param data1 (int) The ID of the contact that wants to join.
|
||||
* @param data2 (int) The progress as:
|
||||
* 300=vg-/vc-request received, typically shown as "bob@addr joins".
|
||||
* 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
|
||||
* 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol.
|
||||
* 1000=Protocol finished for this contact.
|
||||
* @param data2 (int) The progress, always 1000.
|
||||
*/
|
||||
#define DC_EVENT_SECUREJOIN_INVITER_PROGRESS 2060
|
||||
|
||||
@@ -6626,6 +6628,60 @@ void dc_event_unref(dc_event_t* event);
|
||||
*/
|
||||
#define DC_EVENT_CHANNEL_OVERFLOW 2400
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Incoming call.
|
||||
* UI will usually start ringing,
|
||||
* or show a notification if there is already a call in some profile.
|
||||
*
|
||||
* Together with this event,
|
||||
* a message of type #DC_MSG_CALL is added to the corresponding chat;
|
||||
* this message is announced and updated by the usual event as #DC_EVENT_MSGS_CHANGED,
|
||||
* there is usually no need to take care of this message from any of the CALL events.
|
||||
*
|
||||
* If user takes action, dc_accept_incoming_call() or dc_end_call() should be called.
|
||||
*
|
||||
* Otherwise, ringing should end on #DC_EVENT_CALL_ENDED
|
||||
* or #DC_EVENT_INCOMING_CALL_ACCEPTED
|
||||
*
|
||||
* @param data1 (int) msg_id ID of the message referring to the call.
|
||||
* @param data2 (char*) place_call_info, text passed to dc_place_outgoing_call()
|
||||
* @param data2 (int) 1 if incoming call is a video call, 0 otherwise
|
||||
*/
|
||||
#define DC_EVENT_INCOMING_CALL 2550
|
||||
|
||||
/**
|
||||
* The callee accepted an incoming call on this or another device using dc_accept_incoming_call().
|
||||
* The caller gets the event #DC_EVENT_OUTGOING_CALL_ACCEPTED at the same time.
|
||||
*
|
||||
* UI usually only takes action in case call UI was opened before, otherwise the event should be ignored.
|
||||
*
|
||||
* @param data1 (int) msg_id ID of the message referring to the call
|
||||
*/
|
||||
#define DC_EVENT_INCOMING_CALL_ACCEPTED 2560
|
||||
|
||||
/**
|
||||
* A call placed using dc_place_outgoing_call() was accepted by the callee using dc_accept_incoming_call().
|
||||
*
|
||||
* UI usually only takes action in case call UI was opened before, otherwise the event should be ignored.
|
||||
*
|
||||
* @param data1 (int) msg_id ID of the message referring to the call
|
||||
* @param data2 (char*) accept_call_info, text passed to dc_accept_incoming_call()
|
||||
*/
|
||||
#define DC_EVENT_OUTGOING_CALL_ACCEPTED 2570
|
||||
|
||||
/**
|
||||
* An incoming or outgoing call was ended using dc_end_call() on this or another device, by caller or callee.
|
||||
* Moreover, the event is sent when the call was not accepted within 1 minute timeout.
|
||||
*
|
||||
* UI usually only takes action in case call UI was opened before, otherwise the event should be ignored.
|
||||
*
|
||||
* @param data1 (int) msg_id ID of the message referring to the call
|
||||
*/
|
||||
#define DC_EVENT_CALL_ENDED 2580
|
||||
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
@@ -6893,9 +6949,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used in summaries.
|
||||
#define DC_STR_GIF 23
|
||||
|
||||
/// "Encrypted message"
|
||||
///
|
||||
/// Used in subjects of outgoing messages.
|
||||
/// @deprecated 2025-07, this string is no longer needed.
|
||||
#define DC_STR_ENCRYPTEDMSG 24
|
||||
|
||||
/// "End-to-end encryption available."
|
||||
@@ -7048,6 +7102,8 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// "Unknown sender for this chat. See 'info' for more details."
|
||||
///
|
||||
/// Use as message text if assigning the message to a chat is not totally correct.
|
||||
///
|
||||
/// @deprecated 2025-08-18
|
||||
#define DC_STR_UNKNOWN_SENDER_FOR_CHAT 72
|
||||
|
||||
/// "Message from %1$s"
|
||||
@@ -7110,17 +7166,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// @deprecated Deprecated 2021-01-30, DC_STR_EPHEMERAL_WEEKS is used instead.
|
||||
#define DC_STR_EPHEMERAL_FOUR_WEEKS 81
|
||||
|
||||
/// "Video chat invitation"
|
||||
///
|
||||
/// Used in summaries.
|
||||
#define DC_STR_VIDEOCHAT_INVITATION 82
|
||||
|
||||
/// "You are invited to a video chat, click %1$s to join."
|
||||
///
|
||||
/// Used as message text of outgoing video chat invitations.
|
||||
/// - %1$s will be replaced by the URL of the video chat
|
||||
#define DC_STR_VIDEOCHAT_INVITE_MSG_BODY 83
|
||||
|
||||
/// "Error: %1$s"
|
||||
///
|
||||
/// Used in error strings.
|
||||
@@ -7590,6 +7635,18 @@ 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.
|
||||
@@ -7600,7 +7657,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used as a device message after a successful backup transfer.
|
||||
#define DC_STR_BACKUP_TRANSFER_MSG_BODY 163
|
||||
|
||||
/// "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
/// "Messages are end-to-end encrypted."
|
||||
///
|
||||
/// Used in info messages.
|
||||
#define DC_STR_CHAT_PROTECTION_ENABLED 170
|
||||
@@ -7608,6 +7665,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// "%1$s sent a message from another device."
|
||||
///
|
||||
/// Used in info messages.
|
||||
/// @deprecated 2025-07
|
||||
#define DC_STR_CHAT_PROTECTION_DISABLED 171
|
||||
|
||||
/// "Others will only see this group after you sent a first message."
|
||||
@@ -7629,6 +7687,12 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// `%1$s` will be replaced by the provider's domain.
|
||||
#define DC_STR_INVALID_UNENCRYPTED_MAIL 174
|
||||
|
||||
/// "⚠️ It seems you are using Delta Chat on multiple devices that cannot decrypt each other's outgoing messages. To fix this, on the older device use \"Settings / Add Second Device\" and follow the instructions."
|
||||
///
|
||||
/// Added to the device chat if could not decrypt a new outgoing message (i.e. not when fetching
|
||||
/// existing messages). But no more than once a day.
|
||||
#define DC_STR_CANT_DECRYPT_OUTGOING_MSGS 175
|
||||
|
||||
/// "You reacted %1$s to '%2$s'"
|
||||
///
|
||||
/// `%1$s` will be replaced by the reaction, usually an emoji
|
||||
@@ -7662,8 +7726,35 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// @deprecated 2025-06-05
|
||||
#define DC_STR_SECUREJOIN_TAKES_LONGER 192
|
||||
|
||||
/// "Contact". Deprecated, currently unused.
|
||||
#define DC_STR_CONTACT 200
|
||||
/// "❤️ Seems you're enjoying Delta Chat!"… (donation request device message)
|
||||
#define DC_STR_DONATION_REQUEST 193
|
||||
|
||||
/// "Outgoing call"
|
||||
#define DC_STR_OUTGOING_CALL 194
|
||||
|
||||
/// "Incoming call"
|
||||
#define DC_STR_INCOMING_CALL 195
|
||||
|
||||
/// "Declined call"
|
||||
#define DC_STR_DECLINED_CALL 196
|
||||
|
||||
/// "Canceled call"
|
||||
#define DC_STR_CANCELED_CALL 197
|
||||
|
||||
/// "Missed call"
|
||||
#define DC_STR_MISSED_CALL 198
|
||||
|
||||
/// "You left the channel."
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_CHANNEL_LEFT_BY_YOU 200
|
||||
|
||||
/// "Scan to join channel %1$s"
|
||||
///
|
||||
/// Subtitle for channel join qrcode svg image generated by the core.
|
||||
///
|
||||
/// `%1$s` will be replaced with the channel name.
|
||||
#define DC_STR_SECURE_JOIN_CHANNEL_QR_DESC 201
|
||||
|
||||
/**
|
||||
* @}
|
||||
|
||||
@@ -375,7 +375,7 @@ pub unsafe extern "C" fn dc_get_connectivity(context: *const dc_context_t) -> li
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
block_on(ctx.get_connectivity()) as u32 as libc::c_int
|
||||
ctx.get_connectivity() as u32 as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -556,6 +556,10 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::AccountsChanged => 2302,
|
||||
EventType::AccountsItemChanged => 2303,
|
||||
EventType::EventChannelOverflow { .. } => 2400,
|
||||
EventType::IncomingCall { .. } => 2550,
|
||||
EventType::IncomingCallAccepted { .. } => 2560,
|
||||
EventType::OutgoingCallAccepted { .. } => 2570,
|
||||
EventType::CallEnded { .. } => 2580,
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||
@@ -619,7 +623,11 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
EventType::WebxdcRealtimeData { msg_id, .. }
|
||||
| EventType::WebxdcStatusUpdate { msg_id, .. }
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { msg_id }
|
||||
| EventType::WebxdcInstanceDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
| EventType::WebxdcInstanceDeleted { msg_id, .. }
|
||||
| EventType::IncomingCall { msg_id, .. }
|
||||
| EventType::IncomingCallAccepted { msg_id, .. }
|
||||
| EventType::OutgoingCallAccepted { msg_id, .. }
|
||||
| EventType::CallEnded { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
EventType::ChatlistItemChanged { chat_id } => {
|
||||
chat_id.unwrap_or_default().to_u32() as libc::c_int
|
||||
}
|
||||
@@ -671,6 +679,9 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::ChatModified(_)
|
||||
| EventType::ChatDeleted { .. }
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
|
||||
| EventType::IncomingCallAccepted { .. }
|
||||
| EventType::OutgoingCallAccepted { .. }
|
||||
| EventType::CallEnded { .. }
|
||||
| EventType::EventChannelOverflow { .. } => 0,
|
||||
EventType::MsgsChanged { msg_id, .. }
|
||||
| EventType::ReactionsChanged { msg_id, .. }
|
||||
@@ -689,6 +700,8 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
..
|
||||
} => status_update_serial.to_u32() as libc::c_int,
|
||||
EventType::WebxdcRealtimeData { data, .. } => data.len() as libc::c_int,
|
||||
EventType::IncomingCall { has_video, .. } => *has_video as libc::c_int,
|
||||
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||
@@ -767,8 +780,21 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::ChatlistChanged
|
||||
| EventType::AccountsChanged
|
||||
| EventType::AccountsItemChanged
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
|
||||
| EventType::EventChannelOverflow { .. } => ptr::null_mut(),
|
||||
| EventType::IncomingCallAccepted { .. }
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. } => ptr::null_mut(),
|
||||
EventType::IncomingCall {
|
||||
place_call_info, ..
|
||||
} => {
|
||||
let data2 = place_call_info.to_c_string().unwrap_or_default();
|
||||
data2.into_raw()
|
||||
}
|
||||
EventType::OutgoingCallAccepted {
|
||||
accept_call_info, ..
|
||||
} => {
|
||||
let data2 = accept_call_info.to_c_string().unwrap_or_default();
|
||||
data2.into_raw()
|
||||
}
|
||||
EventType::CallEnded { .. } | EventType::EventChannelOverflow { .. } => ptr::null_mut(),
|
||||
EventType::ConfigureProgress { comment, .. } => {
|
||||
if let Some(comment) = comment {
|
||||
comment.to_c_string().unwrap_or_default().into_raw()
|
||||
@@ -1072,25 +1098,6 @@ pub unsafe extern "C" fn dc_send_delete_request(
|
||||
.ok();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_send_videochat_invitation(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
) -> u32 {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_send_videochat_invitation()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
chat::send_videochat_invitation(ctx, ChatId::new(chat_id))
|
||||
.await
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.unwrap_or_log_default(ctx, "Failed to send video chat invitation")
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_send_webxdc_status_update(
|
||||
context: *mut dc_context_t,
|
||||
@@ -1167,6 +1174,61 @@ pub unsafe extern "C" fn dc_init_webxdc_integration(
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_place_outgoing_call(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
place_call_info: *const libc::c_char,
|
||||
) -> u32 {
|
||||
if context.is_null() || chat_id == 0 {
|
||||
eprintln!("ignoring careless call to dc_place_outgoing_call()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
let chat_id = ChatId::new(chat_id);
|
||||
let place_call_info = to_string_lossy(place_call_info);
|
||||
|
||||
block_on(ctx.place_outgoing_call(chat_id, place_call_info))
|
||||
.context("Failed to place call")
|
||||
.log_err(ctx)
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.unwrap_or_log_default(ctx, "Failed to place call")
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accept_incoming_call(
|
||||
context: *mut dc_context_t,
|
||||
msg_id: u32,
|
||||
accept_call_info: *const libc::c_char,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() || msg_id == 0 {
|
||||
eprintln!("ignoring careless call to dc_accept_incoming_call()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
let msg_id = MsgId::new(msg_id);
|
||||
let accept_call_info = to_string_lossy(accept_call_info);
|
||||
|
||||
block_on(ctx.accept_incoming_call(msg_id, accept_call_info))
|
||||
.context("Failed to accept call")
|
||||
.is_ok() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_end_call(context: *mut dc_context_t, msg_id: u32) -> libc::c_int {
|
||||
if context.is_null() || msg_id == 0 {
|
||||
eprintln!("ignoring careless call to dc_end_call()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
let msg_id = MsgId::new(msg_id);
|
||||
|
||||
block_on(ctx.end_call(msg_id))
|
||||
.context("Failed to end call")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_set_draft(
|
||||
context: *mut dc_context_t,
|
||||
@@ -3165,16 +3227,6 @@ pub unsafe extern "C" fn dc_chat_is_encrypted(chat: *mut dc_chat_t) -> libc::c_i
|
||||
.unwrap_or_log_default(&ffi_chat.context, "Failed dc_chat_is_encrypted") as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_chat_is_protection_broken(chat: *mut dc_chat_t) -> libc::c_int {
|
||||
if chat.is_null() {
|
||||
eprintln!("ignoring careless call to dc_chat_is_protection_broken()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_chat = &*chat;
|
||||
ffi_chat.chat.is_protection_broken() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_chat_is_sending_locations(chat: *mut dc_chat_t) -> libc::c_int {
|
||||
if chat.is_null() {
|
||||
@@ -3783,31 +3835,6 @@ pub unsafe extern "C" fn dc_msg_has_html(msg: *mut dc_msg_t) -> libc::c_int {
|
||||
ffi_msg.message.has_html().into()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_videochat_url(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_videochat_url()");
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
|
||||
ffi_msg
|
||||
.message
|
||||
.get_videochat_url()
|
||||
.unwrap_or_default()
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_videochat_type(msg: *mut dc_msg_t) -> libc::c_int {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_videochat_type()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
ffi_msg.message.get_videochat_type().unwrap_or_default() as i32
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_setupcodebegin(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||
if msg.is_null() {
|
||||
|
||||
@@ -51,7 +51,6 @@ impl Lot {
|
||||
Qr::Account { domain } => Some(Cow::Borrowed(domain)),
|
||||
Qr::Backup2 { .. } => None,
|
||||
Qr::BackupTooNew { .. } => None,
|
||||
Qr::WebrtcInstance { domain, .. } => Some(Cow::Borrowed(domain)),
|
||||
Qr::Proxy { host, port, .. } => Some(Cow::Owned(format!("{host}:{port}"))),
|
||||
Qr::Addr { draft, .. } => draft.as_deref().map(Cow::Borrowed),
|
||||
Qr::Url { url } => Some(Cow::Borrowed(url)),
|
||||
@@ -105,7 +104,6 @@ impl Lot {
|
||||
Qr::Account { .. } => LotState::QrAccount,
|
||||
Qr::Backup2 { .. } => LotState::QrBackup2,
|
||||
Qr::BackupTooNew { .. } => LotState::QrBackupTooNew,
|
||||
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
|
||||
Qr::Proxy { .. } => LotState::QrProxy,
|
||||
Qr::Addr { .. } => LotState::QrAddr,
|
||||
Qr::Url { .. } => LotState::QrUrl,
|
||||
@@ -132,7 +130,6 @@ impl Lot {
|
||||
Qr::Account { .. } => Default::default(),
|
||||
Qr::Backup2 { .. } => Default::default(),
|
||||
Qr::BackupTooNew { .. } => Default::default(),
|
||||
Qr::WebrtcInstance { .. } => Default::default(),
|
||||
Qr::Proxy { .. } => Default::default(),
|
||||
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
|
||||
Qr::Url { .. } => Default::default(),
|
||||
@@ -185,9 +182,6 @@ pub enum LotState {
|
||||
|
||||
QrBackupTooNew = 255,
|
||||
|
||||
/// text1=domain, text2=instance pattern
|
||||
QrWebrtcInstance = 260,
|
||||
|
||||
/// text1=address, text2=protocol
|
||||
QrProxy = 271,
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.0.0"
|
||||
version = "2.20.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -8,6 +8,7 @@ use std::{collections::HashMap, str::FromStr};
|
||||
use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||
pub use deltachat::accounts::Accounts;
|
||||
use deltachat::blob::BlobObject;
|
||||
use deltachat::calls::ice_servers;
|
||||
use deltachat::chat::{
|
||||
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
|
||||
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
|
||||
@@ -47,6 +48,7 @@ pub mod types;
|
||||
|
||||
use num_traits::FromPrimitive;
|
||||
use types::account::Account;
|
||||
use types::calls::JsonrpcCallInfo;
|
||||
use types::chat::FullChat;
|
||||
use types::contact::{ContactObject, VcardContact};
|
||||
use types::events::Event;
|
||||
@@ -91,7 +93,8 @@ pub struct CommandApi {
|
||||
|
||||
/// Receiver side of the event channel.
|
||||
///
|
||||
/// Events from it can be received by calling `get_next_event` method.
|
||||
/// Events from it can be received by calling
|
||||
/// [`CommandApi::get_next_event`] method.
|
||||
event_emitter: Arc<EventEmitter>,
|
||||
|
||||
states: Arc<Mutex<BTreeMap<u32, AccountState>>>,
|
||||
@@ -123,7 +126,7 @@ impl CommandApi {
|
||||
.read()
|
||||
.await
|
||||
.get_account(id)
|
||||
.ok_or_else(|| anyhow!("account with id {} not found", id))?;
|
||||
.ok_or_else(|| anyhow!("account with id {id} not found"))?;
|
||||
Ok(sc)
|
||||
}
|
||||
|
||||
@@ -173,7 +176,15 @@ impl CommandApi {
|
||||
get_info()
|
||||
}
|
||||
|
||||
/// Get the next event.
|
||||
/// Get the next event, and remove it from the event queue.
|
||||
///
|
||||
/// If no events have happened since the last `get_next_event`
|
||||
/// (i.e. if the event queue is empty), the response will be returned
|
||||
/// only when a new event fires.
|
||||
///
|
||||
/// Note that if you are using the `BaseDeltaChat` JavaScript class
|
||||
/// or the `Rpc` Python class, this function will be invoked
|
||||
/// by those classes internally and should not be used manually.
|
||||
async fn get_next_event(&self) -> Result<Event> {
|
||||
self.event_emitter
|
||||
.recv()
|
||||
@@ -224,6 +235,14 @@ impl CommandApi {
|
||||
self.accounts.read().await.get_selected_account_id()
|
||||
}
|
||||
|
||||
/// Set the order of accounts.
|
||||
/// The provided list should contain all account IDs in the desired order.
|
||||
/// If an account ID is missing from the list, it will be appended at the end.
|
||||
/// If the list contains non-existent account IDs, they will be ignored.
|
||||
async fn set_accounts_order(&self, order: Vec<u32>) -> Result<()> {
|
||||
self.accounts.write().await.set_accounts_order(order).await
|
||||
}
|
||||
|
||||
/// Get a list of all configured accounts.
|
||||
async fn get_all_accounts(&self) -> Result<Vec<Account>> {
|
||||
let mut accounts = Vec::new();
|
||||
@@ -289,8 +308,7 @@ impl CommandApi {
|
||||
Ok(Account::from_context(&ctx, account_id).await?)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"account with id {} doesn't exist anymore",
|
||||
account_id
|
||||
"account with id {account_id} doesn't exist anymore"
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -945,7 +963,7 @@ impl CommandApi {
|
||||
Ok(contacts.iter().map(|id| id.to_u32()).collect::<Vec<u32>>())
|
||||
}
|
||||
|
||||
/// Create a new group chat.
|
||||
/// Create a new encrypted group chat (with key-contacts).
|
||||
///
|
||||
/// After creation,
|
||||
/// the group has one member with the ID DC_CONTACT_ID_SELF
|
||||
@@ -963,14 +981,24 @@ impl CommandApi {
|
||||
///
|
||||
/// @param protect If set to 1 the function creates group with protection initially enabled.
|
||||
/// Only verified members are allowed in these groups
|
||||
/// and end-to-end-encryption is always enabled.
|
||||
async fn create_group_chat(&self, account_id: u32, name: String, protect: bool) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let protect = match protect {
|
||||
true => ProtectionStatus::Protected,
|
||||
false => ProtectionStatus::Unprotected,
|
||||
};
|
||||
chat::create_group_chat(&ctx, protect, &name)
|
||||
chat::create_group_ex(&ctx, Some(protect), &name)
|
||||
.await
|
||||
.map(|id| id.to_u32())
|
||||
}
|
||||
|
||||
/// Create a new unencrypted group chat.
|
||||
///
|
||||
/// Same as [`Self::create_group_chat`], but the chat is unencrypted and can only have
|
||||
/// address-contacts.
|
||||
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)
|
||||
.await
|
||||
.map(|id| id.to_u32())
|
||||
}
|
||||
@@ -1209,8 +1237,10 @@ impl CommandApi {
|
||||
}
|
||||
|
||||
/// Returns all messages of a particular chat.
|
||||
/// If `add_daymarker` is `true`, it will return them as
|
||||
/// `DC_MSG_ID_DAYMARKER`, e.g. [1234, 1237, 9, 1239].
|
||||
///
|
||||
/// * `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.
|
||||
async fn get_message_ids(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -1453,7 +1483,14 @@ impl CommandApi {
|
||||
|
||||
/// Add a single contact as a result of an explicit user action.
|
||||
///
|
||||
/// Returns contact id of the created or existing contact
|
||||
/// 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.
|
||||
async fn create_contact(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -1603,9 +1640,19 @@ impl CommandApi {
|
||||
Contact::get_encrinfo(&ctx, ContactId::new(contact_id)).await
|
||||
}
|
||||
|
||||
/// Check if an e-mail address belongs to a known and unblocked 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().
|
||||
async fn lookup_contact_id_by_addr(
|
||||
@@ -1761,13 +1808,13 @@ impl CommandApi {
|
||||
|
||||
/// Offers a backup for remote devices to retrieve.
|
||||
///
|
||||
/// Can be cancelled by stopping the ongoing process. Success or failure can be tracked
|
||||
/// Can be canceled by stopping the ongoing process. Success or failure can be tracked
|
||||
/// via the `ImexProgress` event which should either reach `1000` for success or `0` for
|
||||
/// failure.
|
||||
///
|
||||
/// This **stops IO** while it is running.
|
||||
///
|
||||
/// Returns once a remote device has retrieved the backup, or is cancelled.
|
||||
/// Returns once a remote device has retrieved the backup, or is canceled.
|
||||
async fn provide_backup(&self, account_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
|
||||
@@ -1833,7 +1880,7 @@ impl CommandApi {
|
||||
/// This retrieves the backup from a remote device over the network and imports it into
|
||||
/// the current device.
|
||||
///
|
||||
/// Can be cancelled by stopping the ongoing process.
|
||||
/// Can be canceled by stopping the ongoing process.
|
||||
///
|
||||
/// Do not forget to call start_io on the account after a successful import,
|
||||
/// otherwise it will not connect to the email server.
|
||||
@@ -1871,7 +1918,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().await as u32)
|
||||
Ok(ctx.get_connectivity() as u32)
|
||||
}
|
||||
|
||||
/// Get an overview of the current connectivity, and possibly more statistics.
|
||||
@@ -1954,6 +2001,11 @@ impl CommandApi {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Leaves the gossip of the webxdc with the given message id.
|
||||
///
|
||||
/// NB: When this is called before closing a webxdc app in UIs, it must be guaranteed that
|
||||
/// `send_webxdc_realtime_*()` functions aren't called for the given `instance_message_id`
|
||||
/// anymore until the app is open again.
|
||||
async fn leave_webxdc_realtime(&self, account_id: u32, instance_message_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
leave_webxdc_realtime(&ctx, MsgId::new(instance_message_id)).await
|
||||
@@ -2031,6 +2083,53 @@ impl CommandApi {
|
||||
.map(|msg_id| msg_id.to_u32()))
|
||||
}
|
||||
|
||||
/// Starts an outgoing call.
|
||||
async fn place_outgoing_call(
|
||||
&self,
|
||||
account_id: u32,
|
||||
chat_id: u32,
|
||||
place_call_info: String,
|
||||
) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let msg_id = ctx
|
||||
.place_outgoing_call(ChatId::new(chat_id), place_call_info)
|
||||
.await?;
|
||||
Ok(msg_id.to_u32())
|
||||
}
|
||||
|
||||
/// Accepts an incoming call.
|
||||
async fn accept_incoming_call(
|
||||
&self,
|
||||
account_id: u32,
|
||||
msg_id: u32,
|
||||
accept_call_info: String,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.accept_incoming_call(MsgId::new(msg_id), accept_call_info)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ends incoming or outgoing call.
|
||||
async fn end_call(&self, account_id: u32, msg_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.end_call(MsgId::new(msg_id)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns information about the call.
|
||||
async fn call_info(&self, account_id: u32, msg_id: u32) -> Result<JsonrpcCallInfo> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let call_info = JsonrpcCallInfo::from_msg_id(&ctx, MsgId::new(msg_id)).await?;
|
||||
Ok(call_info)
|
||||
}
|
||||
|
||||
/// Returns JSON with ICE servers, to be used for WebRTC video calls.
|
||||
async fn ice_servers(&self, account_id: u32) -> Result<String> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ice_servers(&ctx).await
|
||||
}
|
||||
|
||||
/// Makes an HTTP GET request and returns a response.
|
||||
///
|
||||
/// `url` is the HTTP or HTTPS URL.
|
||||
@@ -2182,13 +2281,6 @@ impl CommandApi {
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_videochat_invitation(&self, account_id: u32, chat_id: u32) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
chat::send_videochat_invitation(&ctx, ChatId::new(chat_id))
|
||||
.await
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// misc prototyping functions
|
||||
// that might get removed later again
|
||||
@@ -2219,8 +2311,7 @@ impl CommandApi {
|
||||
let message = Message::load_from_db(&ctx, MsgId::new(msg_id)).await?;
|
||||
ensure!(
|
||||
message.get_viewtype() == Viewtype::Sticker,
|
||||
"message {} is not a sticker",
|
||||
msg_id
|
||||
"message {msg_id} is not a sticker"
|
||||
);
|
||||
let account_folder = ctx
|
||||
.get_dbfile()
|
||||
@@ -2440,10 +2531,7 @@ impl CommandApi {
|
||||
.to_u32();
|
||||
Ok(msg_id)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"chat with id {} doesn't have draft message",
|
||||
chat_id
|
||||
))
|
||||
Err(anyhow!("chat with id {chat_id} doesn't have draft message"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
97
deltachat-jsonrpc/src/api/types/calls.rs
Normal file
97
deltachat-jsonrpc/src/api/types/calls.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
|
||||
use deltachat::calls::{call_state, sdp_has_video, CallState};
|
||||
use deltachat::context::Context;
|
||||
use deltachat::message::MsgId;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "CallInfo", rename_all = "camelCase")]
|
||||
pub struct JsonrpcCallInfo {
|
||||
/// SDP offer.
|
||||
///
|
||||
/// Can be used to manually answer the call
|
||||
/// even if incoming call event was missed.
|
||||
pub sdp_offer: String,
|
||||
|
||||
/// True if SDP offer has a video.
|
||||
pub has_video: bool,
|
||||
|
||||
/// Call state.
|
||||
///
|
||||
/// For example, if the call is accepted, active, canceled, declined etc.
|
||||
pub state: JsonrpcCallState,
|
||||
}
|
||||
|
||||
impl JsonrpcCallInfo {
|
||||
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<JsonrpcCallInfo> {
|
||||
let call_info = context.load_call_by_id(msg_id).await?.with_context(|| {
|
||||
format!("Attempting to get call state of non-call message {msg_id}")
|
||||
})?;
|
||||
let sdp_offer = call_info.place_call_info.clone();
|
||||
let has_video = sdp_has_video(&sdp_offer).unwrap_or_default();
|
||||
let state = JsonrpcCallState::from_msg_id(context, msg_id).await?;
|
||||
|
||||
Ok(JsonrpcCallInfo {
|
||||
sdp_offer,
|
||||
has_video,
|
||||
state,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "CallState", tag = "kind")]
|
||||
pub enum JsonrpcCallState {
|
||||
/// Fresh incoming or outgoing call that is still ringing.
|
||||
///
|
||||
/// There is no separate state for outgoing call
|
||||
/// that has been dialled but not ringing on the other side yet
|
||||
/// as we don't know whether the other side received our call.
|
||||
Alerting,
|
||||
|
||||
/// Active call.
|
||||
Active,
|
||||
|
||||
/// Completed call that was once active
|
||||
/// and then was terminated for any reason.
|
||||
Completed {
|
||||
/// Call duration in seconds.
|
||||
duration: i64,
|
||||
},
|
||||
|
||||
/// Incoming call that was not picked up within a timeout
|
||||
/// or was explicitly ended by the caller before we picked up.
|
||||
Missed,
|
||||
|
||||
/// Incoming call that was explicitly ended on our side
|
||||
/// before picking up or outgoing call
|
||||
/// that was declined before the timeout.
|
||||
Declined,
|
||||
|
||||
/// Outgoing call that has been canceled on our side
|
||||
/// before receiving a response.
|
||||
///
|
||||
/// Incoming calls cannot be canceled,
|
||||
/// on the receiver side canceled calls
|
||||
/// usually result in missed calls.
|
||||
Canceled,
|
||||
}
|
||||
|
||||
impl JsonrpcCallState {
|
||||
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<JsonrpcCallState> {
|
||||
let call_state = call_state(context, msg_id).await?;
|
||||
|
||||
let jsonrpc_call_state = match call_state {
|
||||
CallState::Alerting => JsonrpcCallState::Alerting,
|
||||
CallState::Active => JsonrpcCallState::Active,
|
||||
CallState::Completed { duration } => JsonrpcCallState::Completed { duration },
|
||||
CallState::Missed => JsonrpcCallState::Missed,
|
||||
CallState::Declined => JsonrpcCallState::Declined,
|
||||
CallState::Canceled => JsonrpcCallState::Canceled,
|
||||
};
|
||||
|
||||
Ok(jsonrpc_call_state)
|
||||
}
|
||||
}
|
||||
@@ -21,15 +21,16 @@ pub struct FullChat {
|
||||
|
||||
/// True if the chat is protected.
|
||||
///
|
||||
/// UI should display a green checkmark
|
||||
/// in the chat title,
|
||||
/// in the chat profile title and
|
||||
/// in the chatlist item
|
||||
/// if chat protection is enabled.
|
||||
/// UI should also display a green checkmark
|
||||
/// in the contact profile
|
||||
/// if 1:1 chat with this contact exists and is protected.
|
||||
/// Only verified contacts
|
||||
/// as determined by [`ContactObject::is_verified`] / `Contact.isVerified`
|
||||
/// can be added to protected chats.
|
||||
///
|
||||
/// Protected chats are created using [`create_group_chat`] / `createGroupChat()`
|
||||
/// by setting the 'protect' parameter to true.
|
||||
///
|
||||
/// [`create_group_chat`]: crate::api::CommandApi::create_group_chat
|
||||
is_protected: bool,
|
||||
|
||||
/// True if the chat is encrypted.
|
||||
/// This means that all messages in the chat are encrypted,
|
||||
/// and all contacts in the chat are "key-contacts",
|
||||
@@ -70,7 +71,7 @@ pub struct FullChat {
|
||||
fresh_message_counter: usize,
|
||||
// is_group - please check over chat.type in frontend instead
|
||||
is_contact_request: bool,
|
||||
is_protection_broken: bool,
|
||||
|
||||
is_device_chat: bool,
|
||||
self_in_group: bool,
|
||||
is_muted: bool,
|
||||
@@ -144,7 +145,6 @@ impl FullChat {
|
||||
color,
|
||||
fresh_message_counter,
|
||||
is_contact_request: chat.is_contact_request(),
|
||||
is_protection_broken: chat.is_protection_broken(),
|
||||
is_device_chat: chat.is_device_talk(),
|
||||
self_in_group: contact_ids.contains(&ContactId::SELF),
|
||||
is_muted: chat.is_muted(),
|
||||
@@ -215,7 +215,7 @@ pub struct BasicChat {
|
||||
is_self_talk: bool,
|
||||
color: String,
|
||||
is_contact_request: bool,
|
||||
is_protection_broken: bool,
|
||||
|
||||
is_device_chat: bool,
|
||||
is_muted: bool,
|
||||
}
|
||||
@@ -244,7 +244,6 @@ impl BasicChat {
|
||||
is_self_talk: chat.is_self_talk(),
|
||||
color,
|
||||
is_contact_request: chat.is_contact_request(),
|
||||
is_protection_broken: chat.is_protection_broken(),
|
||||
is_device_chat: chat.is_device_talk(),
|
||||
is_muted: chat.is_muted(),
|
||||
})
|
||||
|
||||
@@ -23,6 +23,7 @@ pub enum ChatListItemFetchResult {
|
||||
name: String,
|
||||
avatar_path: Option<String>,
|
||||
color: String,
|
||||
chat_type: u32,
|
||||
last_updated: Option<i64>,
|
||||
summary_text1: String,
|
||||
summary_text2: String,
|
||||
@@ -54,6 +55,7 @@ pub enum ChatListItemFetchResult {
|
||||
///
|
||||
/// See also `is_key_contact` on `Contact`.
|
||||
is_encrypted: bool,
|
||||
/// deprecated 2025-07, use chat_type instead
|
||||
is_group: bool,
|
||||
fresh_message_counter: usize,
|
||||
is_self_talk: bool,
|
||||
@@ -64,10 +66,6 @@ pub enum ChatListItemFetchResult {
|
||||
is_pinned: bool,
|
||||
is_muted: bool,
|
||||
is_contact_request: bool,
|
||||
/// Deprecated 2025-07, alias for is_out_broadcast
|
||||
is_broadcast: bool,
|
||||
/// true if the chat type is OutBroadcast
|
||||
is_out_broadcast: bool,
|
||||
/// contact id if this is a dm chat (for view profile entry in context menu)
|
||||
dm_chat_contact: Option<u32>,
|
||||
was_seen_recently: bool,
|
||||
@@ -157,6 +155,7 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
name: chat.get_name().to_owned(),
|
||||
avatar_path,
|
||||
color,
|
||||
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
||||
last_updated,
|
||||
summary_text1,
|
||||
summary_text2,
|
||||
@@ -174,8 +173,6 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
is_pinned: visibility == ChatVisibility::Pinned,
|
||||
is_muted: chat.is_muted(),
|
||||
is_contact_request: chat.is_contact_request(),
|
||||
is_broadcast: chat.get_type() == Chattype::OutBroadcast,
|
||||
is_out_broadcast: chat.get_type() == Chattype::OutBroadcast,
|
||||
dm_chat_contact,
|
||||
was_seen_recently,
|
||||
last_message_type: message_type,
|
||||
|
||||
@@ -31,27 +31,36 @@ pub struct ContactObject {
|
||||
/// e.g. if we just scanned the fingerprint from a QR code.
|
||||
e2ee_avail: bool,
|
||||
|
||||
/// True if the contact can be added to verified groups.
|
||||
/// True if the contact
|
||||
/// can be added to protected chats
|
||||
/// because SELF and contact have verified their fingerprints in both directions.
|
||||
///
|
||||
/// If this is true
|
||||
/// UI should display green checkmark after the contact name
|
||||
/// in contact list items,
|
||||
/// in chat member list items
|
||||
/// and in profiles if no chat with the contact exist.
|
||||
/// See [`Self::verifier_id`]/`Contact.verifierId` for a guidance how to display these information.
|
||||
is_verified: bool,
|
||||
|
||||
/// True if the contact profile title should have a green checkmark.
|
||||
/// The contact ID that verified a contact.
|
||||
///
|
||||
/// This indicates whether 1:1 chat has a green checkmark
|
||||
/// or will have a green checkmark if created.
|
||||
is_profile_verified: bool,
|
||||
|
||||
/// The ID of the contact that verified this contact.
|
||||
/// As verifier may be unknown,
|
||||
/// use [`Self::is_verified`]/`Contact.isVerified` to check if a contact can be added to a protected chat.
|
||||
///
|
||||
/// If this is present,
|
||||
/// display a green checkmark and "Introduced by ..."
|
||||
/// string followed by the verifier contact name and address
|
||||
/// in the contact profile.
|
||||
/// UI should display the information in the contact's profile as follows:
|
||||
///
|
||||
/// - If `verifierId` != 0,
|
||||
/// display text "Introduced by ..."
|
||||
/// with the name and address of the contact
|
||||
/// formatted by `name_and_addr`/`nameAndAddr`.
|
||||
/// Prefix the text by a green checkmark.
|
||||
///
|
||||
/// - If `verifierId` == 0 and `isVerified` != 0,
|
||||
/// display "Introduced" prefixed by a green checkmark.
|
||||
///
|
||||
/// - if `verifierId` == 0 and `isVerified` == 0,
|
||||
/// display nothing
|
||||
///
|
||||
/// This contains the contact ID of the verifier.
|
||||
/// If it is `DC_CONTACT_ID_SELF`, we verified the contact ourself.
|
||||
/// If it is None/Null, we don't have verifier information or
|
||||
/// the contact is not verified.
|
||||
verifier_id: Option<u32>,
|
||||
|
||||
/// the contact's last seen timestamp
|
||||
@@ -72,7 +81,6 @@ impl ContactObject {
|
||||
None => None,
|
||||
};
|
||||
let is_verified = contact.is_verified(context).await?;
|
||||
let is_profile_verified = contact.is_profile_verified(context).await?;
|
||||
|
||||
let verifier_id = contact
|
||||
.get_verifier_id(context)
|
||||
@@ -94,7 +102,6 @@ impl ContactObject {
|
||||
is_key_contact: contact.is_key_contact(),
|
||||
e2ee_avail: contact.e2ee_avail(context).await?,
|
||||
is_verified,
|
||||
is_profile_verified,
|
||||
verifier_id,
|
||||
last_seen: contact.last_seen(),
|
||||
was_seen_recently: contact.was_seen_recently(),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use deltachat::{Event as CoreEvent, EventType as CoreEventType};
|
||||
use num_traits::ToPrimitive;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
@@ -224,7 +225,6 @@ pub enum EventType {
|
||||
},
|
||||
|
||||
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
||||
/// Or the verify state of a chat has changed.
|
||||
/// See setChatName(), setChatProfileImage(), addContactToChat()
|
||||
/// and removeContactFromChat().
|
||||
///
|
||||
@@ -294,8 +294,8 @@ pub enum EventType {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ImexFileWritten { path: String },
|
||||
|
||||
/// Progress information of a secure-join handshake from the view of the inviter
|
||||
/// (Alice, the person who shows the QR code).
|
||||
/// Progress event sent when SecureJoin protocol has finished
|
||||
/// from the view of the inviter (Alice, the person who shows the QR code).
|
||||
///
|
||||
/// These events are typically sent after a joiner has scanned the QR code
|
||||
/// generated by getChatSecurejoinQrCodeSvg().
|
||||
@@ -304,11 +304,14 @@ pub enum EventType {
|
||||
/// ID of the contact that wants to join.
|
||||
contact_id: u32,
|
||||
|
||||
/// Progress as:
|
||||
/// 300=vg-/vc-request received, typically shown as "bob@addr joins".
|
||||
/// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
|
||||
/// 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol.
|
||||
/// 1000=Protocol finished for this contact.
|
||||
/// The type of the joined chat.
|
||||
/// This can take the same values
|
||||
/// as `BasicChat.chatType` ([`crate::api::types::chat::BasicChat::chat_type`]).
|
||||
chat_type: u32,
|
||||
/// ID of the chat in case of success.
|
||||
chat_id: u32,
|
||||
|
||||
/// Progress, always 1000.
|
||||
progress: usize,
|
||||
},
|
||||
|
||||
@@ -417,6 +420,45 @@ pub enum EventType {
|
||||
/// Number of events skipped.
|
||||
n: u64,
|
||||
},
|
||||
|
||||
/// Incoming call.
|
||||
IncomingCall {
|
||||
/// ID of the info message referring to the call.
|
||||
msg_id: u32,
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: u32,
|
||||
/// User-defined info as passed to place_outgoing_call()
|
||||
place_call_info: String,
|
||||
/// True if incoming call is a video call.
|
||||
has_video: bool,
|
||||
},
|
||||
|
||||
/// Incoming call accepted.
|
||||
/// This is esp. interesting to stop ringing on other devices.
|
||||
IncomingCallAccepted {
|
||||
/// ID of the info message referring to the call.
|
||||
msg_id: u32,
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: u32,
|
||||
},
|
||||
|
||||
/// Outgoing call accepted.
|
||||
OutgoingCallAccepted {
|
||||
/// ID of the info message referring to the call.
|
||||
msg_id: u32,
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: u32,
|
||||
/// User-defined info passed to dc_accept_incoming_call(
|
||||
accept_call_info: String,
|
||||
},
|
||||
|
||||
/// Call ended.
|
||||
CallEnded {
|
||||
/// ID of the info message referring to the call.
|
||||
msg_id: u32,
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: u32,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<CoreEventType> for EventType {
|
||||
@@ -523,9 +565,13 @@ impl From<CoreEventType> for EventType {
|
||||
},
|
||||
CoreEventType::SecurejoinInviterProgress {
|
||||
contact_id,
|
||||
chat_type,
|
||||
chat_id,
|
||||
progress,
|
||||
} => SecurejoinInviterProgress {
|
||||
contact_id: contact_id.to_u32(),
|
||||
chat_type: chat_type.to_u32().unwrap_or(0),
|
||||
chat_id: chat_id.to_u32(),
|
||||
progress,
|
||||
},
|
||||
CoreEventType::SecurejoinJoinerProgress {
|
||||
@@ -567,6 +613,34 @@ impl From<CoreEventType> for EventType {
|
||||
CoreEventType::EventChannelOverflow { n } => EventChannelOverflow { n },
|
||||
CoreEventType::AccountsChanged => AccountsChanged,
|
||||
CoreEventType::AccountsItemChanged => AccountsItemChanged,
|
||||
CoreEventType::IncomingCall {
|
||||
msg_id,
|
||||
chat_id,
|
||||
place_call_info,
|
||||
has_video,
|
||||
} => IncomingCall {
|
||||
msg_id: msg_id.to_u32(),
|
||||
chat_id: chat_id.to_u32(),
|
||||
place_call_info,
|
||||
has_video,
|
||||
},
|
||||
CoreEventType::IncomingCallAccepted { msg_id, chat_id } => IncomingCallAccepted {
|
||||
msg_id: msg_id.to_u32(),
|
||||
chat_id: chat_id.to_u32(),
|
||||
},
|
||||
CoreEventType::OutgoingCallAccepted {
|
||||
msg_id,
|
||||
chat_id,
|
||||
accept_call_info,
|
||||
} => OutgoingCallAccepted {
|
||||
msg_id: msg_id.to_u32(),
|
||||
chat_id: chat_id.to_u32(),
|
||||
accept_call_info,
|
||||
},
|
||||
CoreEventType::CallEnded { msg_id, chat_id } => CallEnded {
|
||||
msg_id: msg_id.to_u32(),
|
||||
chat_id: chat_id.to_u32(),
|
||||
},
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||
|
||||
@@ -84,9 +84,6 @@ pub struct MessageObject {
|
||||
dimensions_height: i32,
|
||||
dimensions_width: i32,
|
||||
|
||||
videochat_type: Option<u32>,
|
||||
videochat_url: Option<String>,
|
||||
|
||||
override_sender_name: Option<String>,
|
||||
sender: ContactObject,
|
||||
|
||||
@@ -239,15 +236,6 @@ impl MessageObject {
|
||||
dimensions_height: message.get_height(),
|
||||
dimensions_width: message.get_width(),
|
||||
|
||||
videochat_type: match message.get_videochat_type() {
|
||||
Some(vct) => Some(
|
||||
vct.to_u32()
|
||||
.context("videochat type conversion to number failed")?,
|
||||
),
|
||||
None => None,
|
||||
},
|
||||
videochat_url: message.get_videochat_url(),
|
||||
|
||||
override_sender_name,
|
||||
sender,
|
||||
|
||||
@@ -321,8 +309,8 @@ pub enum MessageViewtype {
|
||||
/// Message containing any file, eg. a PDF.
|
||||
File,
|
||||
|
||||
/// Message is an invitation to a videochat.
|
||||
VideochatInvitation,
|
||||
/// Message is a call.
|
||||
Call,
|
||||
|
||||
/// Message is an webxdc instance.
|
||||
Webxdc,
|
||||
@@ -345,7 +333,7 @@ impl From<Viewtype> for MessageViewtype {
|
||||
Viewtype::Voice => MessageViewtype::Voice,
|
||||
Viewtype::Video => MessageViewtype::Video,
|
||||
Viewtype::File => MessageViewtype::File,
|
||||
Viewtype::VideochatInvitation => MessageViewtype::VideochatInvitation,
|
||||
Viewtype::Call => MessageViewtype::Call,
|
||||
Viewtype::Webxdc => MessageViewtype::Webxdc,
|
||||
Viewtype::Vcard => MessageViewtype::Vcard,
|
||||
}
|
||||
@@ -364,7 +352,7 @@ impl From<MessageViewtype> for Viewtype {
|
||||
MessageViewtype::Voice => Viewtype::Voice,
|
||||
MessageViewtype::Video => Viewtype::Video,
|
||||
MessageViewtype::File => Viewtype::File,
|
||||
MessageViewtype::VideochatInvitation => Viewtype::VideochatInvitation,
|
||||
MessageViewtype::Call => Viewtype::Call,
|
||||
MessageViewtype::Webxdc => Viewtype::Webxdc,
|
||||
MessageViewtype::Vcard => Viewtype::Vcard,
|
||||
}
|
||||
@@ -416,6 +404,9 @@ pub enum SystemMessageType {
|
||||
/// Chat ephemeral message timer is changed.
|
||||
EphemeralTimerChanged,
|
||||
|
||||
// Chat is e2ee
|
||||
ChatE2ee,
|
||||
|
||||
// Chat protection state changed
|
||||
ChatProtectionEnabled,
|
||||
ChatProtectionDisabled,
|
||||
@@ -434,6 +425,9 @@ pub enum SystemMessageType {
|
||||
|
||||
/// This message contains a users iroh node address.
|
||||
IrohNodeAddr,
|
||||
|
||||
CallAccepted,
|
||||
CallEnded,
|
||||
}
|
||||
|
||||
impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
||||
@@ -450,6 +444,7 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
||||
SystemMessage::LocationStreamingEnabled => SystemMessageType::LocationStreamingEnabled,
|
||||
SystemMessage::LocationOnly => SystemMessageType::LocationOnly,
|
||||
SystemMessage::EphemeralTimerChanged => SystemMessageType::EphemeralTimerChanged,
|
||||
SystemMessage::ChatE2ee => SystemMessageType::ChatE2ee,
|
||||
SystemMessage::ChatProtectionEnabled => SystemMessageType::ChatProtectionEnabled,
|
||||
SystemMessage::ChatProtectionDisabled => SystemMessageType::ChatProtectionDisabled,
|
||||
SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync,
|
||||
@@ -459,6 +454,8 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
||||
SystemMessage::IrohNodeAddr => SystemMessageType::IrohNodeAddr,
|
||||
SystemMessage::SecurejoinWait => SystemMessageType::SecurejoinWait,
|
||||
SystemMessage::SecurejoinWaitTimeout => SystemMessageType::SecurejoinWaitTimeout,
|
||||
SystemMessage::CallAccepted => SystemMessageType::CallAccepted,
|
||||
SystemMessage::CallEnded => SystemMessageType::CallEnded,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod account;
|
||||
pub mod calls;
|
||||
pub mod chat;
|
||||
pub mod chat_list;
|
||||
pub mod contact;
|
||||
|
||||
@@ -225,13 +225,6 @@ impl From<Qr> for QrObject {
|
||||
auth_token,
|
||||
},
|
||||
Qr::BackupTooNew {} => QrObject::BackupTooNew {},
|
||||
Qr::WebrtcInstance {
|
||||
domain,
|
||||
instance_pattern,
|
||||
} => QrObject::WebrtcInstance {
|
||||
domain,
|
||||
instance_pattern,
|
||||
},
|
||||
Qr::Proxy { url, host, port } => QrObject::Proxy { url, host, port },
|
||||
Qr::Addr { contact_id, draft } => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.0.0"
|
||||
"version": "2.20.0"
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ export class BaseDeltaChat<
|
||||
Transport extends BaseTransport<any>,
|
||||
> extends TinyEmitter<Events> {
|
||||
rpc: RawClient;
|
||||
account?: T.Account;
|
||||
private contextEmitters: { [key: number]: TinyEmitter<ContextEvents> } = {};
|
||||
|
||||
//@ts-ignore
|
||||
@@ -36,6 +35,10 @@ export class BaseDeltaChat<
|
||||
|
||||
constructor(
|
||||
public transport: Transport,
|
||||
/**
|
||||
* Whether to start calling {@linkcode RawClient.getNextEvent}
|
||||
* and emitting the respective events on this class.
|
||||
*/
|
||||
startEventLoop: boolean,
|
||||
) {
|
||||
super();
|
||||
@@ -45,6 +48,9 @@ export class BaseDeltaChat<
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see the constructor's `startEventLoop`
|
||||
*/
|
||||
async eventLoop(): Promise<void> {
|
||||
while (true) {
|
||||
const event = await this.rpc.getNextEvent();
|
||||
@@ -63,10 +69,17 @@ export class BaseDeltaChat<
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use {@linkcode BaseDeltaChat.rpc.getAllAccounts} instead.
|
||||
*/
|
||||
async listAccounts(): Promise<T.Account[]> {
|
||||
return await this.rpc.getAllAccounts();
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenience function to listen on events binned by `account_id`
|
||||
* (see {@linkcode RawClient.getAllAccounts}).
|
||||
*/
|
||||
getContextEvents(account_id: number) {
|
||||
if (this.contextEmitters[account_id]) {
|
||||
return this.contextEmitters[account_id];
|
||||
|
||||
@@ -95,8 +95,10 @@ describe("online tests", function () {
|
||||
false,
|
||||
);
|
||||
|
||||
expect(messageList).have.length(1);
|
||||
const message = await dc.rpc.getMessage(accountId2, messageList[0]);
|
||||
// There are 2 messages in the chat:
|
||||
// 'Messages are end-to-end encrypted' (info message) and 'Hello'
|
||||
expect(messageList).have.length(2);
|
||||
const message = await dc.rpc.getMessage(accountId2, messageList[1]);
|
||||
expect(message.text).equal("Hello");
|
||||
expect(message.showPadlock).equal(true);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "2.0.0"
|
||||
version = "2.20.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
@@ -87,7 +87,7 @@ async fn poke_eml_file(context: &Context, filename: &Path) -> Result<()> {
|
||||
let data = read_file(context, filename).await?;
|
||||
|
||||
if let Err(err) = receive_imf(context, &data, false).await {
|
||||
println!("receive_imf errored: {err:?}");
|
||||
eprintln!("receive_imf errored: {err:?}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -210,13 +210,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
} else {
|
||||
""
|
||||
},
|
||||
if msg.get_viewtype() == Viewtype::VideochatInvitation {
|
||||
format!(
|
||||
"[VIDEOCHAT-INVITATION: {}, type={}]",
|
||||
msg.get_videochat_url().unwrap_or_default(),
|
||||
msg.get_videochat_type().unwrap_or_default()
|
||||
)
|
||||
} else if msg.get_viewtype() == Viewtype::Webxdc {
|
||||
if msg.get_viewtype() == Viewtype::Webxdc {
|
||||
match msg.get_webxdc_info(context).await {
|
||||
Ok(info) => format!(
|
||||
"[WEBXDC: {}, icon={}, document={}, summary={}, source_code_url={}]",
|
||||
@@ -371,7 +365,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
sendhtml <file for html-part> [<text for plain-part>]\n\
|
||||
sendsyncmsg\n\
|
||||
sendupdate <msg-id> <json status update>\n\
|
||||
videochat\n\
|
||||
draft [<text>]\n\
|
||||
devicemsg <text>\n\
|
||||
listmedia\n\
|
||||
@@ -403,6 +396,8 @@ 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\
|
||||
@@ -423,7 +418,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
Ok(setup_code) => {
|
||||
println!("Setup code for the transferred setup message: {setup_code}",)
|
||||
}
|
||||
Err(err) => bail!("Failed to generate setup code: {}", err),
|
||||
Err(err) => bail!("Failed to generate setup code: {err}"),
|
||||
},
|
||||
"get-setupcodebegin" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
@@ -437,7 +432,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
setupcodebegin.unwrap_or_default(),
|
||||
);
|
||||
} else {
|
||||
bail!("{} is no setup message.", msg_id,);
|
||||
bail!("{msg_id} is no setup message.",);
|
||||
}
|
||||
}
|
||||
"continue-key-transfer" => {
|
||||
@@ -532,7 +527,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
println!("Report written to: {file:#?}");
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("Failed to get connectivity html: {}", err);
|
||||
bail!("Failed to get connectivity html: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -621,7 +616,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
println!("Location streaming enabled.");
|
||||
}
|
||||
println!("{cnt} chats");
|
||||
println!("{time_needed:?} to create this list");
|
||||
eprintln!("{time_needed:?} to create this list");
|
||||
}
|
||||
"start-realtime" => {
|
||||
if arg1.is_empty() {
|
||||
@@ -731,7 +726,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
chat::marknoticed_chat(&context, sel_chat.get_id()).await?;
|
||||
let time_noticed_needed = time_noticed_start.elapsed().unwrap_or_default();
|
||||
|
||||
println!(
|
||||
eprintln!(
|
||||
"{time_needed:?} to create this list, {time_noticed_needed:?} to mark all messages as noticed."
|
||||
);
|
||||
}
|
||||
@@ -960,10 +955,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
let msg_id = MsgId::new(arg1.parse()?);
|
||||
context.send_webxdc_status_update(msg_id, arg2).await?;
|
||||
}
|
||||
"videochat" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
chat::send_videochat_invitation(&context, sel_chat.as_ref().unwrap().get_id()).await?;
|
||||
}
|
||||
"listmsgs" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <query> missing.");
|
||||
|
||||
@@ -985,7 +976,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
},
|
||||
query,
|
||||
);
|
||||
println!("{time_needed:?} to create this list");
|
||||
eprintln!("{time_needed:?} to create this list");
|
||||
}
|
||||
"draft" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
@@ -1151,7 +1142,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"listcontacts" | "contacts" => {
|
||||
let contacts = Contact::get_all(&context, DC_GCL_ADD_SELF, Some(arg1)).await?;
|
||||
log_contactlist(&context, &contacts).await?;
|
||||
println!("{} contacts.", contacts.len());
|
||||
println!("{} key contacts.", contacts.len());
|
||||
let addrcontacts = Contact::get_all(&context, DC_GCL_ADDRESS, Some(arg1)).await?;
|
||||
log_contactlist(&context, &addrcontacts).await?;
|
||||
println!("{} address contacts.", addrcontacts.len());
|
||||
}
|
||||
"addcontact" => {
|
||||
ensure!(!arg1.is_empty(), "Arguments [<name>] <addr> expected.");
|
||||
@@ -1215,6 +1209,24 @@ 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?;
|
||||
@@ -1224,7 +1236,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
||||
match set_config_from_qr(&context, arg1).await {
|
||||
Ok(()) => println!("Config set from QR code, you can now call 'configure'"),
|
||||
Err(err) => println!("Cannot set config from QR code: {err:?}"),
|
||||
Err(err) => eprintln!("Cannot set config from QR code: {err:?}"),
|
||||
}
|
||||
}
|
||||
"createqrsvg" => {
|
||||
@@ -1275,7 +1287,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
);
|
||||
}
|
||||
"" => (),
|
||||
_ => bail!("Unknown command: \"{}\" type ? for help.", arg0),
|
||||
_ => bail!("Unknown command: \"{arg0}\" type ? for help."),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -179,7 +179,7 @@ const DB_COMMANDS: [&str; 11] = [
|
||||
"housekeeping",
|
||||
];
|
||||
|
||||
const CHAT_COMMANDS: [&str; 39] = [
|
||||
const CHAT_COMMANDS: [&str; 38] = [
|
||||
"listchats",
|
||||
"listarchived",
|
||||
"start-realtime",
|
||||
@@ -206,7 +206,6 @@ const CHAT_COMMANDS: [&str; 39] = [
|
||||
"sendhtml",
|
||||
"sendsyncmsg",
|
||||
"sendupdate",
|
||||
"videochat",
|
||||
"draft",
|
||||
"devicemsg",
|
||||
"listmedia",
|
||||
@@ -232,7 +231,7 @@ const MESSAGE_COMMANDS: [&str; 10] = [
|
||||
"delmsg",
|
||||
"react",
|
||||
];
|
||||
const CONTACT_COMMANDS: [&str; 7] = [
|
||||
const CONTACT_COMMANDS: [&str; 9] = [
|
||||
"listcontacts",
|
||||
"addcontact",
|
||||
"contactinfo",
|
||||
@@ -240,6 +239,8 @@ const CONTACT_COMMANDS: [&str; 7] = [
|
||||
"block",
|
||||
"unblock",
|
||||
"listblocked",
|
||||
"import-vcard",
|
||||
"make-vcard",
|
||||
];
|
||||
const MISC_COMMANDS: [&str; 14] = [
|
||||
"getqr",
|
||||
@@ -311,7 +312,7 @@ impl Validator for DcHelper {}
|
||||
|
||||
async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
if args.len() < 2 {
|
||||
println!("Error: Bad arguments, expected [db-name].");
|
||||
eprintln!("Error: Bad arguments, expected [db-name].");
|
||||
bail!("No db-name specified");
|
||||
}
|
||||
let context = ContextBuilder::new(args[1].clone().into())
|
||||
@@ -366,7 +367,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
false
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Error: {err:#}");
|
||||
eprintln!("Error: {err:#}");
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -381,7 +382,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Error: {err:#}");
|
||||
eprintln!("Error: {err:#}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -465,7 +466,7 @@ async fn handle_cmd(
|
||||
println!("QR code svg written to: {file:#?}");
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("Failed to get QR code svg: {}", err);
|
||||
bail!("Failed to get QR code svg: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "2.0.0"
|
||||
version = "2.20.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -19,6 +19,7 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: 3.14",
|
||||
"Topic :: Communications :: Chat",
|
||||
"Topic :: Communications :: Email"
|
||||
]
|
||||
|
||||
@@ -92,6 +92,12 @@ def _run_cli(
|
||||
)
|
||||
parser.add_argument("--email", action="store", help="email address", default=os.getenv("DELTACHAT_EMAIL"))
|
||||
parser.add_argument("--password", action="store", help="password", default=os.getenv("DELTACHAT_PASSWORD"))
|
||||
parser.add_argument(
|
||||
"--displayname", action="store", help="the profile's display name", default=os.getenv("DELTACHAT_DISPLAYNAME"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--avatar", action="store", help="filename of the profile's avatar", default=os.getenv("DELTACHAT_AVATAR"),
|
||||
)
|
||||
args = parser.parse_args(argv[1:])
|
||||
|
||||
with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
|
||||
@@ -108,7 +114,12 @@ def _run_cli(
|
||||
configure_thread = Thread(
|
||||
target=client.configure,
|
||||
daemon=True,
|
||||
kwargs={"email": args.email, "password": args.password},
|
||||
kwargs={
|
||||
"email": args.email,
|
||||
"password": args.password,
|
||||
"displayname": args.displayname,
|
||||
"selfavatar": args.avatar,
|
||||
},
|
||||
)
|
||||
configure_thread.start()
|
||||
client.run_forever()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
from warnings import warn
|
||||
@@ -185,7 +186,21 @@ class Account:
|
||||
return Contact(self, contact_id)
|
||||
|
||||
def get_contact_by_addr(self, address: str) -> Optional[Contact]:
|
||||
"""Check if an e-mail address belongs to a known and unblocked 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()."""
|
||||
contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address)
|
||||
return contact_id and Contact(self, contact_id)
|
||||
|
||||
@@ -456,3 +471,8 @@ class Account:
|
||||
def initiate_autocrypt_key_transfer(self) -> None:
|
||||
"""Send Autocrypt Setup Message."""
|
||||
return self._rpc.initiate_autocrypt_key_transfer(self.id)
|
||||
|
||||
def ice_servers(self) -> list:
|
||||
"""Return ICE servers for WebRTC configuration."""
|
||||
ice_servers_json = self._rpc.ice_servers(self.id)
|
||||
return json.loads(ice_servers_json)
|
||||
|
||||
@@ -168,6 +168,11 @@ class Chat:
|
||||
msg_id = self._rpc.send_sticker(self.account.id, self.id, path)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
def resend_messages(self, messages: list[Message]) -> None:
|
||||
"""Resend a list of messages to this chat."""
|
||||
msg_ids = [msg.id for msg in messages]
|
||||
self._rpc.resend_messages(self.account.id, msg_ids)
|
||||
|
||||
def forward_messages(self, messages: list[Message]) -> None:
|
||||
"""Forward a list of messages to this chat."""
|
||||
msg_ids = [msg.id for msg in messages]
|
||||
@@ -289,3 +294,8 @@ class Chat:
|
||||
f.write(vcard.encode())
|
||||
f.flush()
|
||||
self._rpc.send_msg(self.account.id, self.id, {"viewtype": ViewType.VCARD, "file": f.name})
|
||||
|
||||
def place_outgoing_call(self, place_call_info: str) -> Message:
|
||||
"""Starts an outgoing call."""
|
||||
msg_id = self._rpc.place_outgoing_call(self.account.id, self.id, place_call_info)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
@@ -73,6 +73,10 @@ class EventType(str, Enum):
|
||||
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
|
||||
ACCOUNTS_CHANGED = "AccountsChanged"
|
||||
ACCOUNTS_ITEM_CHANGED = "AccountsItemChanged"
|
||||
INCOMING_CALL = "IncomingCall"
|
||||
INCOMING_CALL_ACCEPTED = "IncomingCallAccepted"
|
||||
OUTGOING_CALL_ACCEPTED = "OutgoingCallAccepted"
|
||||
CALL_ENDED = "CallEnded"
|
||||
CONFIG_SYNCED = "ConfigSynced"
|
||||
WEBXDC_REALTIME_DATA = "WebxdcRealtimeData"
|
||||
WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED = "WebxdcRealtimeAdvertisementReceived"
|
||||
@@ -156,7 +160,6 @@ class ViewType(str, Enum):
|
||||
VOICE = "Voice"
|
||||
VIDEO = "Video"
|
||||
FILE = "File"
|
||||
VIDEOCHAT_INVITATION = "VideochatInvitation"
|
||||
WEBXDC = "Webxdc"
|
||||
VCARD = "Vcard"
|
||||
|
||||
@@ -275,11 +278,3 @@ class SocketSecurity(IntEnum):
|
||||
SSL = 1
|
||||
STARTTLS = 2
|
||||
PLAIN = 3
|
||||
|
||||
|
||||
class VideochatType(IntEnum):
|
||||
"""Video chat URL type."""
|
||||
|
||||
UNKNOWN = 0
|
||||
BASICWEBRTC = 1
|
||||
JITSI = 2
|
||||
|
||||
@@ -102,3 +102,15 @@ class Message:
|
||||
def send_webxdc_realtime_data(self, data) -> None:
|
||||
"""Send data to the realtime channel."""
|
||||
yield self._rpc.send_webxdc_realtime_data.future(self.account.id, self.id, list(data))
|
||||
|
||||
def accept_incoming_call(self, accept_call_info):
|
||||
"""Accepts an incoming call."""
|
||||
self._rpc.accept_incoming_call(self.account.id, self.id, accept_call_info)
|
||||
|
||||
def end_call(self):
|
||||
"""Ends incoming or outgoing call."""
|
||||
self._rpc.end_call(self.account.id, self.id)
|
||||
|
||||
def get_call_info(self) -> AttrDict:
|
||||
"""Return information about the call."""
|
||||
return AttrDict(self._rpc.call_info(self.account.id, self.id))
|
||||
|
||||
@@ -13,6 +13,12 @@ from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Messag
|
||||
from ._utils import futuremethod
|
||||
from .rpc import Rpc
|
||||
|
||||
E2EE_INFO_MSGS = 1
|
||||
"""
|
||||
The number of info messages added to new e2ee chats.
|
||||
Currently this is "End-to-end encryption available".
|
||||
"""
|
||||
|
||||
|
||||
class ACFactory:
|
||||
"""Test account factory."""
|
||||
@@ -22,9 +28,7 @@ class ACFactory:
|
||||
|
||||
def get_unconfigured_account(self) -> Account:
|
||||
"""Create a new unconfigured account."""
|
||||
account = self.deltachat.add_account()
|
||||
account.set_config("verified_one_on_one_chats", "1")
|
||||
return account
|
||||
return self.deltachat.add_account()
|
||||
|
||||
def get_unconfigured_bot(self) -> Bot:
|
||||
"""Create a new unconfigured bot."""
|
||||
|
||||
86
deltachat-rpc-client/tests/test_calls.py
Normal file
86
deltachat-rpc-client/tests/test_calls.py
Normal file
@@ -0,0 +1,86 @@
|
||||
from deltachat_rpc_client import EventType, Message
|
||||
|
||||
|
||||
def test_calls(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
place_call_info = "offer"
|
||||
accept_call_info = "answer"
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
outgoing_call_message = alice_chat_bob.place_outgoing_call(place_call_info)
|
||||
assert outgoing_call_message.get_call_info().state.kind == "Alerting"
|
||||
|
||||
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
|
||||
assert incoming_call_event.place_call_info == place_call_info
|
||||
assert not incoming_call_event.has_video # Cannot be parsed as SDP, so false by default
|
||||
incoming_call_message = Message(bob, incoming_call_event.msg_id)
|
||||
assert incoming_call_message.get_call_info().state.kind == "Alerting"
|
||||
assert not incoming_call_message.get_call_info().has_video
|
||||
|
||||
incoming_call_message.accept_incoming_call(accept_call_info)
|
||||
assert incoming_call_message.get_call_info().sdp_offer == place_call_info
|
||||
assert incoming_call_message.get_call_info().state.kind == "Active"
|
||||
outgoing_call_accepted_event = alice.wait_for_event(EventType.OUTGOING_CALL_ACCEPTED)
|
||||
assert outgoing_call_accepted_event.accept_call_info == accept_call_info
|
||||
assert outgoing_call_message.get_call_info().state.kind == "Active"
|
||||
|
||||
outgoing_call_message.end_call()
|
||||
assert outgoing_call_message.get_call_info().state.kind == "Completed"
|
||||
|
||||
end_call_event = bob.wait_for_event(EventType.CALL_ENDED)
|
||||
assert end_call_event.msg_id == outgoing_call_message.id
|
||||
assert incoming_call_message.get_call_info().state.kind == "Completed"
|
||||
|
||||
|
||||
def test_video_call(acfactory) -> None:
|
||||
# Example from <https://datatracker.ietf.org/doc/rfc9143/>
|
||||
# with `s= ` replaced with `s=-`.
|
||||
#
|
||||
# `s=` cannot be empty according to RFC 3264,
|
||||
# so it is more clear as `s=-`.
|
||||
place_call_info = """v=0\r
|
||||
o=alice 2890844526 2890844526 IN IP6 2001:db8::3\r
|
||||
s=-\r
|
||||
c=IN IP6 2001:db8::3\r
|
||||
t=0 0\r
|
||||
a=group:BUNDLE foo bar\r
|
||||
\r
|
||||
m=audio 10000 RTP/AVP 0 8 97\r
|
||||
b=AS:200\r
|
||||
a=mid:foo\r
|
||||
a=rtcp-mux\r
|
||||
a=rtpmap:0 PCMU/8000\r
|
||||
a=rtpmap:8 PCMA/8000\r
|
||||
a=rtpmap:97 iLBC/8000\r
|
||||
a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r
|
||||
\r
|
||||
m=video 10002 RTP/AVP 31 32\r
|
||||
b=AS:1000\r
|
||||
a=mid:bar\r
|
||||
a=rtcp-mux\r
|
||||
a=rtpmap:31 H261/90000\r
|
||||
a=rtpmap:32 MPV/90000\r
|
||||
a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r
|
||||
"""
|
||||
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.place_outgoing_call(place_call_info)
|
||||
|
||||
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
|
||||
assert incoming_call_event.place_call_info == place_call_info
|
||||
assert incoming_call_event.has_video
|
||||
|
||||
incoming_call_message = Message(bob, incoming_call_event.msg_id)
|
||||
assert incoming_call_message.get_call_info().has_video
|
||||
|
||||
|
||||
def test_ice_servers(acfactory) -> None:
|
||||
alice = acfactory.get_online_account()
|
||||
|
||||
ice_servers = alice.ice_servers()
|
||||
assert len(ice_servers) == 1
|
||||
@@ -36,6 +36,9 @@ def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
|
||||
assert ac1.get_config("bcc_self") == "1"
|
||||
|
||||
# Second client receives only second message, but not the first.
|
||||
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
|
||||
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == "Messages are end-to-end encrypted."
|
||||
|
||||
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
|
||||
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == msg_out.get_snapshot().text
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import pytest
|
||||
|
||||
from deltachat_rpc_client import Contact, EventType, Message, events
|
||||
from deltachat_rpc_client.const import ChatType, DownloadState, MessageState
|
||||
from deltachat_rpc_client.pytestplugin import E2EE_INFO_MSGS
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
@@ -170,7 +171,10 @@ def test_account(acfactory) -> None:
|
||||
assert alice.get_size()
|
||||
assert alice.is_configured()
|
||||
assert not alice.get_avatar()
|
||||
assert alice.get_contact_by_addr(bob_addr) is None # There is no address-contact, only key-contact
|
||||
# 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_contacts()
|
||||
assert alice.get_contacts(snapshot=True)
|
||||
assert alice.self_contact
|
||||
@@ -248,6 +252,7 @@ def test_chat(acfactory) -> None:
|
||||
bob_chat_alice.get_encryption_info()
|
||||
|
||||
group = alice.create_group("test group")
|
||||
to_resend = group.send_text("will be resent")
|
||||
group.add_contact(alice_contact_bob)
|
||||
group.get_qr_code()
|
||||
|
||||
@@ -259,6 +264,7 @@ def test_chat(acfactory) -> None:
|
||||
|
||||
msg = group.send_message(text="hi")
|
||||
assert (msg.get_snapshot()).text == "hi"
|
||||
group.resend_messages([to_resend])
|
||||
group.forward_messages([msg])
|
||||
|
||||
group.set_draft(text="test draft")
|
||||
@@ -325,6 +331,52 @@ def test_message(acfactory) -> None:
|
||||
assert reactions == snapshot.reactions
|
||||
|
||||
|
||||
def test_receive_imf_failure(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
|
||||
bob.set_config("fail_on_receiving_full_msg", "1")
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
chat_id = event.chat_id
|
||||
msg_id = event.msg_id
|
||||
message = bob.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.chat_id == chat_id
|
||||
assert snapshot.download_state == DownloadState.AVAILABLE
|
||||
assert snapshot.error is not None
|
||||
assert snapshot.show_padlock
|
||||
|
||||
# The failed message doesn't break the IMAP loop.
|
||||
bob.set_config("fail_on_receiving_full_msg", "0")
|
||||
alice_chat_bob.send_text("Hello again!")
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
assert event.chat_id == chat_id
|
||||
msg_id = event.msg_id
|
||||
message1 = bob.get_message_by_id(msg_id)
|
||||
snapshot = message1.get_snapshot()
|
||||
assert snapshot.chat_id == chat_id
|
||||
assert snapshot.download_state == DownloadState.DONE
|
||||
assert snapshot.error is None
|
||||
|
||||
# The failed message can be re-downloaded later.
|
||||
bob._rpc.download_full_message(bob.id, message.id)
|
||||
event = bob.wait_for_event(EventType.MSGS_CHANGED)
|
||||
message = bob.get_message_by_id(event.msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.download_state == DownloadState.IN_PROGRESS
|
||||
event = bob.wait_for_event(EventType.MSGS_CHANGED)
|
||||
assert event.chat_id == chat_id
|
||||
msg_id = event.msg_id
|
||||
message = bob.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.chat_id == chat_id
|
||||
assert snapshot.download_state == DownloadState.DONE
|
||||
assert snapshot.error is None
|
||||
assert snapshot.text == "Hello!"
|
||||
|
||||
|
||||
def test_selfavatar_sync(acfactory, data, log) -> None:
|
||||
alice = acfactory.get_online_account()
|
||||
|
||||
@@ -457,8 +509,12 @@ def test_wait_next_messages(acfactory) -> None:
|
||||
alice_chat_bot.send_text("Hello!")
|
||||
|
||||
next_messages = next_messages_task.result()
|
||||
assert len(next_messages) == 1
|
||||
snapshot = next_messages[0].get_snapshot()
|
||||
|
||||
if len(next_messages) == E2EE_INFO_MSGS:
|
||||
next_messages += bot.wait_next_messages()
|
||||
|
||||
assert len(next_messages) == 1 + E2EE_INFO_MSGS
|
||||
snapshot = next_messages[0 + E2EE_INFO_MSGS].get_snapshot()
|
||||
assert snapshot.text == "Hello!"
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.0.0"
|
||||
version = "2.20.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.0.0"
|
||||
"version": "2.20.0"
|
||||
}
|
||||
|
||||
@@ -41,22 +41,22 @@ async fn main_impl() -> Result<()> {
|
||||
if let Some(first_arg) = args.next() {
|
||||
if first_arg.to_str() == Some("--version") {
|
||||
if let Some(arg) = args.next() {
|
||||
return Err(anyhow!("Unrecognized argument {:?}", arg));
|
||||
return Err(anyhow!("Unrecognized argument {arg:?}"));
|
||||
}
|
||||
eprintln!("{}", &*DC_VERSION_STR);
|
||||
return Ok(());
|
||||
} else if first_arg.to_str() == Some("--openrpc") {
|
||||
if let Some(arg) = args.next() {
|
||||
return Err(anyhow!("Unrecognized argument {:?}", arg));
|
||||
return Err(anyhow!("Unrecognized argument {arg:?}"));
|
||||
}
|
||||
println!("{}", CommandApi::openrpc_specification()?);
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(anyhow!("Unrecognized option {:?}", first_arg));
|
||||
return Err(anyhow!("Unrecognized option {first_arg:?}"));
|
||||
}
|
||||
}
|
||||
if let Some(arg) = args.next() {
|
||||
return Err(anyhow!("Unrecognized argument {:?}", arg));
|
||||
return Err(anyhow!("Unrecognized argument {arg:?}"));
|
||||
}
|
||||
|
||||
// Install signal handlers early so that the shutdown is graceful starting from here.
|
||||
|
||||
@@ -25,13 +25,11 @@ skip = [
|
||||
{ name = "derive_more-impl", version = "1.0.0" },
|
||||
{ name = "derive_more", version = "1.0.0" },
|
||||
{ name = "event-listener", version = "2.5.3" },
|
||||
{ name = "generator", version = "0.7.5" },
|
||||
{ name = "getrandom", version = "0.2.12" },
|
||||
{ name = "hashbrown", version = "0.14.5" },
|
||||
{ name = "heck", version = "0.4.1" },
|
||||
{ name = "http", version = "0.2.12" },
|
||||
{ name = "linux-raw-sys", version = "0.4.14" },
|
||||
{ name = "loom", version = "0.5.6" },
|
||||
{ name = "lru", version = "0.12.3" },
|
||||
{ name = "netlink-packet-route", version = "0.17.1" },
|
||||
{ name = "nom", version = "7.1.3" },
|
||||
@@ -40,8 +38,6 @@ skip = [
|
||||
{ name = "rand", version = "0.8.5" },
|
||||
{ name = "redox_syscall", version = "0.3.5" },
|
||||
{ name = "redox_syscall", version = "0.4.1" },
|
||||
{ name = "regex-automata", version = "0.1.10" },
|
||||
{ name = "regex-syntax", version = "0.6.29" },
|
||||
{ name = "rustix", version = "0.38.44" },
|
||||
{ name = "serdect", version = "0.2.0" },
|
||||
{ name = "spin", version = "0.9.8" },
|
||||
@@ -50,6 +46,7 @@ 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" },
|
||||
|
||||
@@ -587,6 +587,7 @@
|
||||
(python3.withPackages (pypkgs: with pypkgs; [
|
||||
tox
|
||||
]))
|
||||
nodejs
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[dev-dependencies]
|
||||
bolero = "0.13.3"
|
||||
bolero = "0.13.4"
|
||||
|
||||
[dependencies]
|
||||
mailparse = { workspace = true }
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "2.0.0"
|
||||
version = "2.20.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.8"
|
||||
|
||||
@@ -330,7 +330,21 @@ class Account:
|
||||
return bool(lib.dc_delete_contact(self._dc_context, contact_id))
|
||||
|
||||
def get_contact_by_addr(self, email: str) -> Optional[Contact]:
|
||||
"""get a contact for the email address or None if it's blocked or doesn't exist."""
|
||||
"""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()."""
|
||||
_, addr = parseaddr(email)
|
||||
addr = as_dc_charpointer(addr)
|
||||
contact_id = lib.dc_lookup_contact_id_by_addr(self._dc_context, addr)
|
||||
|
||||
@@ -8,7 +8,6 @@ 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:
|
||||
@@ -164,17 +163,6 @@ 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))
|
||||
@@ -447,10 +435,6 @@ class Message:
|
||||
"""return True if it's a video message."""
|
||||
return self._view_type == const.DC_MSG_VIDEO
|
||||
|
||||
def is_videochat_invitation(self):
|
||||
"""return True if it's a videochat invitation message."""
|
||||
return self._view_type == const.DC_MSG_VIDEOCHAT_INVITATION
|
||||
|
||||
def is_webxdc(self):
|
||||
"""return True if it's a Webxdc message."""
|
||||
return self._view_type == const.DC_MSG_WEBXDC
|
||||
@@ -491,7 +475,6 @@ _view_type_mapping = {
|
||||
"video": const.DC_MSG_VIDEO,
|
||||
"file": const.DC_MSG_FILE,
|
||||
"sticker": const.DC_MSG_STICKER,
|
||||
"videochat": const.DC_MSG_VIDEOCHAT_INVITATION,
|
||||
"webxdc": const.DC_MSG_WEBXDC,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
"""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))
|
||||
@@ -20,6 +20,12 @@ import deltachat
|
||||
from . import Account, account_hookimpl, const, get_core_info
|
||||
from .events import FFIEventLogger, FFIEventTracker
|
||||
|
||||
E2EE_INFO_MSGS = 1
|
||||
"""
|
||||
The number of info messages added to new e2ee chats.
|
||||
Currently this is "End-to-end encryption available".
|
||||
"""
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("deltachat testplugin options")
|
||||
@@ -606,7 +612,7 @@ class ACFactory:
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
msg = ac2.get_message_by_id(ev.data2)
|
||||
assert msg is not None
|
||||
assert msg.text == "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
assert msg.text == "Messages are end-to-end encrypted."
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg is not None
|
||||
assert "Member Me " in msg.text and " added by " in msg.text
|
||||
|
||||
@@ -133,8 +133,7 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
assert "added" in msg.text.lower()
|
||||
|
||||
assert any(
|
||||
m.is_system_message() and m.text == "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
for m in msg.chat.get_messages()
|
||||
m.is_system_message() and m.text == "Messages are end-to-end encrypted." for m in msg.chat.get_messages()
|
||||
)
|
||||
lp.sec("ac1: send message")
|
||||
msg_out = chat1.send_text("hello")
|
||||
@@ -338,7 +337,7 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
|
||||
assert contact.addr == ac1.get_config("addr")
|
||||
chat2 = msg_in.chat
|
||||
assert chat2.is_protected()
|
||||
assert chat2.get_messages()[0].text == "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
assert chat2.get_messages()[0].text == "Messages are end-to-end encrypted."
|
||||
assert open(contact.get_profile_image(), "rb").read() == open(avatar_path, "rb").read()
|
||||
|
||||
lp.sec("ac2_offl: sending message")
|
||||
@@ -412,7 +411,7 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
|
||||
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg_in = ac2_offl.get_message_by_id(ev.data2)
|
||||
assert msg_in.is_system_message()
|
||||
assert msg_in.text == "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
assert msg_in.text == "Messages are end-to-end encrypted."
|
||||
|
||||
# We need to consume one event that has data2=0
|
||||
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
|
||||
@@ -10,6 +10,7 @@ from imap_tools import AND, U
|
||||
import deltachat as dc
|
||||
from deltachat import account_hookimpl, Message
|
||||
from deltachat.tracker import ImexTracker
|
||||
from deltachat.testplugin import E2EE_INFO_MSGS
|
||||
|
||||
|
||||
def test_basic_imap_api(acfactory, tmp_path):
|
||||
@@ -159,32 +160,6 @@ def test_html_message(acfactory, lp):
|
||||
assert html_text in msg2.html
|
||||
|
||||
|
||||
def test_videochat_invitation_message(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
text = "You are invited to a video chat, click https://meet.jit.si/WxEGad0gGzX to join."
|
||||
|
||||
lp.sec("ac1: prepare and send text message to ac2")
|
||||
msg1 = chat.send_text("message0")
|
||||
assert not msg1.is_videochat_invitation()
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == "message0"
|
||||
assert not msg2.is_videochat_invitation()
|
||||
|
||||
lp.sec("ac1: prepare and send videochat invitation to ac2")
|
||||
msg1 = Message.new_empty(ac1, "videochat")
|
||||
msg1.set_text(text)
|
||||
msg1 = chat.send_msg(msg1)
|
||||
assert msg1.is_videochat_invitation()
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == text
|
||||
assert msg2.is_videochat_invitation()
|
||||
|
||||
|
||||
def test_webxdc_message(acfactory, data, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
@@ -408,6 +383,10 @@ def test_forward_messages(acfactory, lp):
|
||||
msg_out = chat.send_text("message2")
|
||||
|
||||
lp.sec("ac2: wait for receive")
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg_in = ac2.get_message_by_id(ev.data2)
|
||||
assert msg_in.text == "Messages are end-to-end encrypted."
|
||||
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
assert ev.data2 == msg_out.id
|
||||
msg_in = ac2.get_message_by_id(msg_out.id)
|
||||
@@ -427,7 +406,7 @@ def test_forward_messages(acfactory, lp):
|
||||
lp.sec("ac2: check new chat has a forwarded message")
|
||||
assert chat3.is_promoted()
|
||||
messages = chat3.get_messages()
|
||||
assert len(messages) == 2
|
||||
assert len(messages) == 3
|
||||
msg = messages[-1]
|
||||
assert msg.is_forwarded()
|
||||
ac2.delete_messages(messages)
|
||||
@@ -622,6 +601,11 @@ def test_moved_markseen(acfactory):
|
||||
|
||||
with ac2.direct_imap.idle() as idle2:
|
||||
ac2.start_io()
|
||||
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg = ac2.get_message_by_id(ev.data2)
|
||||
assert msg.text == "Messages are end-to-end encrypted."
|
||||
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg = ac2.get_message_by_id(ev.data2)
|
||||
|
||||
@@ -738,7 +722,7 @@ def test_mdn_asymmetric(acfactory, lp):
|
||||
lp.sec("sending text message from ac1 to ac2")
|
||||
msg_out = chat.send_text("message1")
|
||||
|
||||
assert len(chat.get_messages()) == 1
|
||||
assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS
|
||||
|
||||
lp.sec("disable ac1 MDNs")
|
||||
ac1.set_config("mdns_enabled", "0")
|
||||
@@ -746,7 +730,7 @@ def test_mdn_asymmetric(acfactory, lp):
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
assert len(msg.chat.get_messages()) == 1
|
||||
assert len(msg.chat.get_messages()) == 1 + E2EE_INFO_MSGS
|
||||
|
||||
lp.sec("ac2: mark incoming message as seen")
|
||||
ac2.mark_seen_messages([msg])
|
||||
@@ -755,7 +739,7 @@ def test_mdn_asymmetric(acfactory, lp):
|
||||
# MDN should be moved even though MDNs are already disabled
|
||||
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
|
||||
assert len(chat.get_messages()) == 1
|
||||
assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS
|
||||
|
||||
# Wait for the message to be marked as seen on IMAP.
|
||||
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder DeltaChat as seen.")
|
||||
@@ -1123,6 +1107,11 @@ def test_send_and_receive_image(acfactory, lp, data):
|
||||
assert m == msg_out
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED|DC_EVENT_INCOMING_MSG")
|
||||
msg_in = ac2.get_message_by_id(ev.data2)
|
||||
assert msg_in.text == "Messages are end-to-end encrypted."
|
||||
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED|DC_EVENT_INCOMING_MSG")
|
||||
assert ev.data2 == msg_out.id
|
||||
msg_in = ac2.get_message_by_id(msg_out.id)
|
||||
@@ -1158,10 +1147,10 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
|
||||
assert contact2.addr == some1_addr
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 3
|
||||
assert messages[0].text == "msg1"
|
||||
assert messages[1].filemime == "image/png"
|
||||
assert os.stat(messages[1].filename).st_size == os.stat(original_image_path).st_size
|
||||
assert len(messages) == 3 + E2EE_INFO_MSGS
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert messages[1 + E2EE_INFO_MSGS].filemime == "image/png"
|
||||
assert os.stat(messages[1 + E2EE_INFO_MSGS].filename).st_size == os.stat(original_image_path).st_size
|
||||
ac.set_config("displayname", "new displayname")
|
||||
assert ac.get_config("displayname") == "new displayname"
|
||||
|
||||
@@ -1414,8 +1403,8 @@ def test_connectivity(acfactory, lp):
|
||||
ac1.maybe_network()
|
||||
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTED)
|
||||
msgs = ac1.create_chat(ac2).get_messages()
|
||||
assert len(msgs) == 1
|
||||
assert msgs[0].text == "Hi"
|
||||
assert len(msgs) == 1 + E2EE_INFO_MSGS
|
||||
assert msgs[0 + E2EE_INFO_MSGS].text == "Hi"
|
||||
|
||||
lp.sec("Test that the connectivity changes to WORKING while new messages are fetched")
|
||||
|
||||
@@ -1425,8 +1414,8 @@ def test_connectivity(acfactory, lp):
|
||||
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_WORKING, dc.const.DC_CONNECTIVITY_CONNECTED)
|
||||
|
||||
msgs = ac1.create_chat(ac2).get_messages()
|
||||
assert len(msgs) == 2
|
||||
assert msgs[1].text == "Hi 2"
|
||||
assert len(msgs) == 2 + E2EE_INFO_MSGS
|
||||
assert msgs[1 + E2EE_INFO_MSGS].text == "Hi 2"
|
||||
|
||||
|
||||
def test_fetch_deleted_msg(acfactory, lp):
|
||||
@@ -1766,12 +1755,12 @@ def test_group_quote(acfactory, lp):
|
||||
"xyz",
|
||||
False,
|
||||
"xyz",
|
||||
), # Test that emails are recognized in a random folder but not moved
|
||||
), # Test that emails aren't found in a random folder
|
||||
(
|
||||
"xyz",
|
||||
"Spam",
|
||||
True,
|
||||
"DeltaChat",
|
||||
), # ...emails are found in a random folder and moved to DeltaChat
|
||||
), # ...emails are moved from the spam folder to "DeltaChat"
|
||||
(
|
||||
"Spam",
|
||||
False,
|
||||
@@ -1796,7 +1785,7 @@ def test_scan_folders(acfactory, lp, folder, move, expected_destination):
|
||||
ac1.stop_io()
|
||||
assert folder in ac1.direct_imap.list_folders()
|
||||
|
||||
lp.sec("Send a message to from ac2 to ac1 and manually move it to the mvbox")
|
||||
lp.sec("Send a message to from ac2 to ac1 and manually move it to `folder`")
|
||||
ac1.direct_imap.select_config_folder("inbox")
|
||||
with ac1.direct_imap.idle() as idle1:
|
||||
acfactory.get_accepted_chat(ac2, ac1).send_text("hello")
|
||||
@@ -1806,10 +1795,17 @@ def test_scan_folders(acfactory, lp, folder, move, expected_destination):
|
||||
lp.sec("start_io() and see if DeltaChat finds the message (" + variant + ")")
|
||||
ac1.set_config("scan_all_folders_debounce_secs", "0")
|
||||
ac1.start_io()
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
chat = ac1.create_chat(ac2)
|
||||
n_msgs = 1 # "Messages are end-to-end encrypted."
|
||||
if folder == "Spam":
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
n_msgs += 1
|
||||
else:
|
||||
ac1._evtracker.wait_idle_inbox_ready()
|
||||
assert len(chat.get_messages()) == n_msgs
|
||||
|
||||
# The message has been downloaded, which means it has reached its destination.
|
||||
# The message has reached its destination.
|
||||
ac1.direct_imap.select_folder(expected_destination)
|
||||
assert len(ac1.direct_imap.get_all_messages()) == 1
|
||||
if folder != expected_destination:
|
||||
|
||||
@@ -6,6 +6,7 @@ import pytest
|
||||
import deltachat as dc
|
||||
from deltachat.tracker import ImexFailed
|
||||
from deltachat import Account, Message
|
||||
from deltachat.testplugin import E2EE_INFO_MSGS
|
||||
|
||||
|
||||
class TestOfflineAccountBasic:
|
||||
@@ -461,9 +462,9 @@ class TestOfflineChat:
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
|
||||
|
||||
def test_import_export_on_encrypted_acct(self, acfactory, tmp_path):
|
||||
passphrase1 = "passphrase1"
|
||||
@@ -500,9 +501,9 @@ class TestOfflineChat:
|
||||
contact2_addr = contact2.addr
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
|
||||
|
||||
ac2.shutdown()
|
||||
|
||||
@@ -517,9 +518,9 @@ class TestOfflineChat:
|
||||
assert contact2.addr == contact2_addr
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
|
||||
|
||||
def test_import_export_with_passphrase(self, acfactory, tmp_path):
|
||||
passphrase = "test_passphrase"
|
||||
@@ -557,9 +558,9 @@ class TestOfflineChat:
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
|
||||
|
||||
def test_import_encrypted_bak_into_encrypted_acct(self, acfactory, tmp_path):
|
||||
"""
|
||||
@@ -603,9 +604,9 @@ class TestOfflineChat:
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
|
||||
|
||||
ac2.shutdown()
|
||||
|
||||
@@ -620,9 +621,9 @@ class TestOfflineChat:
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
|
||||
|
||||
def test_set_get_draft(self, chat1):
|
||||
msg1 = Message.new_empty(chat1.account, "text")
|
||||
@@ -662,4 +663,4 @@ class TestOfflineChat:
|
||||
|
||||
lp.sec("check message count of only system messages (without daymarkers)")
|
||||
sysmessages = [x for x in chat.get_messages() if x.is_system_message()]
|
||||
assert len(sysmessages) == 3
|
||||
assert len(sysmessages) == 4
|
||||
|
||||
@@ -1 +1 @@
|
||||
2025-07-09
|
||||
2025-10-13
|
||||
@@ -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.88.0
|
||||
RUST_VERSION=1.90.0
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
|
||||
|
||||
@@ -6,7 +6,7 @@ set -euo pipefail
|
||||
export TZ=UTC
|
||||
|
||||
# Provider database revision.
|
||||
REV=77cbf92a8565fdf1bcaba10fa93c1455c750a1e9
|
||||
REV=1cce91c1f1065b47e4f307d6fe2f4cca68c74d2e
|
||||
|
||||
CORE_ROOT="$PWD"
|
||||
TMP="$(mktemp -d)"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! # Account manager module.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::future::Future;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -78,7 +78,7 @@ impl Accounts {
|
||||
ensure!(dir.exists(), "directory does not exist");
|
||||
|
||||
let config_file = dir.join(CONFIG_NAME);
|
||||
ensure!(config_file.exists(), "{:?} does not exist", config_file);
|
||||
ensure!(config_file.exists(), "{config_file:?} does not exist");
|
||||
|
||||
let config = Config::from_file(config_file, writable).await?;
|
||||
let events = Events::new();
|
||||
@@ -270,9 +270,51 @@ impl Accounts {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a list of all account ids.
|
||||
/// Gets a list of all account ids in the user-configured order.
|
||||
pub fn get_all(&self) -> Vec<u32> {
|
||||
self.accounts.keys().copied().collect()
|
||||
let mut ordered_ids = Vec::new();
|
||||
let mut all_ids: BTreeSet<u32> = self.accounts.keys().copied().collect();
|
||||
|
||||
// First, add accounts in the configured order
|
||||
for &id in &self.config.inner.accounts_order {
|
||||
if all_ids.remove(&id) {
|
||||
ordered_ids.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Then add any accounts not in the order list (newly added accounts)
|
||||
for id in all_ids {
|
||||
ordered_ids.push(id);
|
||||
}
|
||||
|
||||
ordered_ids
|
||||
}
|
||||
|
||||
/// Sets the order of accounts.
|
||||
///
|
||||
/// The provided list should contain all account IDs in the desired order.
|
||||
/// If an account ID is missing from the list, it will be appended at the end.
|
||||
/// If the list contains non-existent account IDs, they will be ignored.
|
||||
pub async fn set_accounts_order(&mut self, order: Vec<u32>) -> Result<()> {
|
||||
let existing_ids: BTreeSet<u32> = self.accounts.keys().copied().collect();
|
||||
|
||||
// Filter out non-existent account IDs
|
||||
let mut filtered_order: Vec<u32> = order
|
||||
.into_iter()
|
||||
.filter(|id| existing_ids.contains(id))
|
||||
.collect();
|
||||
|
||||
// Add any missing account IDs at the end
|
||||
for &id in &existing_ids {
|
||||
if !filtered_order.contains(&id) {
|
||||
filtered_order.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
self.config.inner.accounts_order = filtered_order;
|
||||
self.config.sync().await?;
|
||||
self.emit_event(EventType::AccountsChanged);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Starts background tasks such as IMAP and SMTP loops for all accounts.
|
||||
@@ -311,11 +353,11 @@ impl Accounts {
|
||||
/// This is an auxiliary function and not part of public API.
|
||||
/// Use [Accounts::background_fetch] instead.
|
||||
async fn background_fetch_no_timeout(accounts: Vec<Context>, events: Events) {
|
||||
let n_accounts = accounts.len();
|
||||
events.emit(Event {
|
||||
id: 0,
|
||||
typ: EventType::Info(format!(
|
||||
"Starting background fetch for {} accounts.",
|
||||
accounts.len()
|
||||
"Starting background fetch for {n_accounts} accounts."
|
||||
)),
|
||||
});
|
||||
let mut set = JoinSet::new();
|
||||
@@ -327,6 +369,12 @@ impl Accounts {
|
||||
});
|
||||
}
|
||||
set.join_all().await;
|
||||
events.emit(Event {
|
||||
id: 0,
|
||||
typ: EventType::Info(format!(
|
||||
"Finished background fetch for {n_accounts} accounts."
|
||||
)),
|
||||
});
|
||||
}
|
||||
|
||||
/// Auxiliary function for [Accounts::background_fetch].
|
||||
@@ -415,6 +463,10 @@ struct InnerConfig {
|
||||
pub selected_account: u32,
|
||||
pub next_id: u32,
|
||||
pub accounts: Vec<AccountConfig>,
|
||||
/// Ordered list of account IDs, representing the user's preferred order.
|
||||
/// If an account ID is not in this list, it will be appended at the end.
|
||||
#[serde(default)]
|
||||
pub accounts_order: Vec<u32>,
|
||||
}
|
||||
|
||||
impl Drop for Config {
|
||||
@@ -481,6 +533,7 @@ impl Config {
|
||||
accounts: Vec::new(),
|
||||
selected_account: 0,
|
||||
next_id: 1,
|
||||
accounts_order: Vec::new(),
|
||||
};
|
||||
if !lock {
|
||||
let cfg = Self {
|
||||
@@ -613,6 +666,10 @@ impl Config {
|
||||
uuid,
|
||||
});
|
||||
self.inner.next_id += 1;
|
||||
|
||||
// Add new account to the end of the order list
|
||||
self.inner.accounts_order.push(id);
|
||||
|
||||
id
|
||||
};
|
||||
|
||||
@@ -634,6 +691,10 @@ impl Config {
|
||||
// remove account from the configs
|
||||
self.inner.accounts.remove(idx);
|
||||
}
|
||||
|
||||
// Remove from order list as well
|
||||
self.inner.accounts_order.retain(|&x| x != id);
|
||||
|
||||
if self.inner.selected_account == id {
|
||||
// reset selected account
|
||||
self.inner.selected_account = self
|
||||
@@ -663,8 +724,7 @@ impl Config {
|
||||
{
|
||||
ensure!(
|
||||
self.inner.accounts.iter().any(|e| e.id == id),
|
||||
"invalid account id: {}",
|
||||
id
|
||||
"invalid account id: {id}"
|
||||
);
|
||||
|
||||
self.inner.selected_account = id;
|
||||
|
||||
@@ -17,7 +17,6 @@ pub enum EncryptPreference {
|
||||
#[default]
|
||||
NoPreference = 0,
|
||||
Mutual = 1,
|
||||
Reset = 20,
|
||||
}
|
||||
|
||||
impl fmt::Display for EncryptPreference {
|
||||
@@ -25,7 +24,6 @@ impl fmt::Display for EncryptPreference {
|
||||
match *self {
|
||||
EncryptPreference::Mutual => write!(fmt, "mutual"),
|
||||
EncryptPreference::NoPreference => write!(fmt, "nopreference"),
|
||||
EncryptPreference::Reset => write!(fmt, "reset"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,7 +35,7 @@ impl FromStr for EncryptPreference {
|
||||
match s {
|
||||
"mutual" => Ok(EncryptPreference::Mutual),
|
||||
"nopreference" => Ok(EncryptPreference::NoPreference),
|
||||
_ => bail!("Cannot parse encryption preference {}", s),
|
||||
_ => bail!("Cannot parse encryption preference {s}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,21 +46,13 @@ pub struct Aheader {
|
||||
pub addr: String,
|
||||
pub public_key: SignedPublicKey,
|
||||
pub prefer_encrypt: EncryptPreference,
|
||||
}
|
||||
|
||||
impl Aheader {
|
||||
/// Creates new autocrypt header
|
||||
pub fn new(
|
||||
addr: String,
|
||||
public_key: SignedPublicKey,
|
||||
prefer_encrypt: EncryptPreference,
|
||||
) -> Self {
|
||||
Aheader {
|
||||
addr,
|
||||
public_key,
|
||||
prefer_encrypt,
|
||||
}
|
||||
}
|
||||
// Whether `_verified` attribute is present.
|
||||
//
|
||||
// `_verified` attribute is an extension to `Autocrypt-Gossip`
|
||||
// header that is used to tell that the sender
|
||||
// marked this key as verified.
|
||||
pub verified: bool,
|
||||
}
|
||||
|
||||
impl fmt::Display for Aheader {
|
||||
@@ -71,6 +61,9 @@ impl fmt::Display for Aheader {
|
||||
if self.prefer_encrypt == EncryptPreference::Mutual {
|
||||
write!(fmt, " prefer-encrypt=mutual;")?;
|
||||
}
|
||||
if self.verified {
|
||||
write!(fmt, " _verified=1;")?;
|
||||
}
|
||||
|
||||
// adds a whitespace every 78 characters, this allows
|
||||
// email crate to wrap the lines according to RFC 5322
|
||||
@@ -125,6 +118,8 @@ impl FromStr for Aheader {
|
||||
.and_then(|raw| raw.parse().ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
let verified = attributes.remove("_verified").is_some();
|
||||
|
||||
// Autocrypt-Level0: unknown attributes starting with an underscore can be safely ignored
|
||||
// Autocrypt-Level0: unknown attribute, treat the header as invalid
|
||||
if attributes.keys().any(|k| !k.starts_with('_')) {
|
||||
@@ -135,6 +130,7 @@ impl FromStr for Aheader {
|
||||
addr,
|
||||
public_key,
|
||||
prefer_encrypt,
|
||||
verified,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -152,10 +148,11 @@ mod tests {
|
||||
|
||||
assert_eq!(h.addr, "me@mail.com");
|
||||
assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual);
|
||||
assert_eq!(h.verified, false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// EncryptPreference::Reset is an internal value, parser should never return it
|
||||
// Non-standard values of prefer-encrypt such as `reset` are treated as no preference.
|
||||
#[test]
|
||||
fn test_from_str_reset() -> Result<()> {
|
||||
let raw = format!("addr=reset@example.com; prefer-encrypt=reset; keydata={RAWKEY}");
|
||||
@@ -245,11 +242,12 @@ mod tests {
|
||||
assert!(
|
||||
format!(
|
||||
"{}",
|
||||
Aheader::new(
|
||||
"test@example.com".to_string(),
|
||||
SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
EncryptPreference::Mutual
|
||||
)
|
||||
Aheader {
|
||||
addr: "test@example.com".to_string(),
|
||||
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
verified: false
|
||||
}
|
||||
)
|
||||
.contains("prefer-encrypt=mutual;")
|
||||
);
|
||||
@@ -260,11 +258,12 @@ mod tests {
|
||||
assert!(
|
||||
!format!(
|
||||
"{}",
|
||||
Aheader::new(
|
||||
"test@example.com".to_string(),
|
||||
SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
EncryptPreference::NoPreference
|
||||
)
|
||||
Aheader {
|
||||
addr: "test@example.com".to_string(),
|
||||
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
prefer_encrypt: EncryptPreference::NoPreference,
|
||||
verified: false
|
||||
}
|
||||
)
|
||||
.contains("prefer-encrypt")
|
||||
);
|
||||
@@ -273,13 +272,27 @@ mod tests {
|
||||
assert!(
|
||||
format!(
|
||||
"{}",
|
||||
Aheader::new(
|
||||
"TeSt@eXaMpLe.cOm".to_string(),
|
||||
SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
EncryptPreference::Mutual
|
||||
)
|
||||
Aheader {
|
||||
addr: "TeSt@eXaMpLe.cOm".to_string(),
|
||||
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
verified: false
|
||||
}
|
||||
)
|
||||
.contains("test@example.com")
|
||||
);
|
||||
|
||||
assert!(
|
||||
format!(
|
||||
"{}",
|
||||
Aheader {
|
||||
addr: "test@example.com".to_string(),
|
||||
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
prefer_encrypt: EncryptPreference::NoPreference,
|
||||
verified: true
|
||||
}
|
||||
)
|
||||
.contains("_verified")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ pub(crate) async fn handle_authres(
|
||||
let from_domain = match EmailAddress::new(from) {
|
||||
Ok(email) => email.domain,
|
||||
Err(e) => {
|
||||
return Err(anyhow::format_err!("invalid email {}: {:#}", from, e));
|
||||
return Err(anyhow::format_err!("invalid email {from}: {e:#}"));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
12
src/blob.rs
12
src/blob.rs
@@ -170,7 +170,7 @@ impl<'a> BlobObject<'a> {
|
||||
false => name,
|
||||
};
|
||||
if !BlobObject::is_acceptible_blob_name(name) {
|
||||
return Err(format_err!("not an acceptable blob name: {}", name));
|
||||
return Err(format_err!("not an acceptable blob name: {name}"));
|
||||
}
|
||||
Ok(BlobObject {
|
||||
blobdir: context.get_blobdir(),
|
||||
@@ -367,11 +367,12 @@ impl<'a> BlobObject<'a> {
|
||||
|| img.get_pixel(x_max, y_max).0[3] == 0)
|
||||
{
|
||||
*vt = Viewtype::Image;
|
||||
} else {
|
||||
// Core doesn't auto-assign `Viewtype::Sticker` to messages and stickers coming
|
||||
// from UIs shouldn't contain sensitive Exif info.
|
||||
return Ok(name);
|
||||
}
|
||||
}
|
||||
if *vt == Viewtype::Sticker && exif.is_none() {
|
||||
return Ok(name);
|
||||
}
|
||||
|
||||
img = match orientation {
|
||||
Some(90) => img.rotate90(),
|
||||
@@ -457,8 +458,7 @@ impl<'a> BlobObject<'a> {
|
||||
{
|
||||
if img_wh < 20 {
|
||||
return Err(format_err!(
|
||||
"Failed to scale image to below {}B.",
|
||||
max_bytes,
|
||||
"Failed to scale image to below {max_bytes}B.",
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -416,6 +416,28 @@ async fn test_recode_image_balanced_png() {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sticker_with_exif() {
|
||||
let bytes = include_bytes!("../../test-data/image/logo.png");
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Sticker,
|
||||
bytes,
|
||||
extension: "png",
|
||||
// TODO: Pretend there's no Exif. Currently `exif` crate doesn't detect Exif in this image,
|
||||
// so the test doesn't check all the logic it should.
|
||||
has_exif: false,
|
||||
original_width: 135,
|
||||
original_height: 135,
|
||||
res_viewtype: Some(Viewtype::Sticker),
|
||||
compressed_width: 135,
|
||||
compressed_height: 135,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Tests that RGBA PNG can be recoded into JPEG
|
||||
/// by dropping alpha channel.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -485,6 +507,7 @@ struct SendImageCheckMediaquality<'a> {
|
||||
pub(crate) original_width: u32,
|
||||
pub(crate) original_height: u32,
|
||||
pub(crate) orientation: i32,
|
||||
pub(crate) res_viewtype: Option<Viewtype>,
|
||||
pub(crate) compressed_width: u32,
|
||||
pub(crate) compressed_height: u32,
|
||||
pub(crate) set_draft: bool,
|
||||
@@ -500,6 +523,7 @@ impl SendImageCheckMediaquality<'_> {
|
||||
let original_width = self.original_width;
|
||||
let original_height = self.original_height;
|
||||
let orientation = self.orientation;
|
||||
let res_viewtype = self.res_viewtype.unwrap_or(Viewtype::Image);
|
||||
let compressed_width = self.compressed_width;
|
||||
let compressed_height = self.compressed_height;
|
||||
let set_draft = self.set_draft;
|
||||
@@ -550,7 +574,7 @@ impl SendImageCheckMediaquality<'_> {
|
||||
}
|
||||
|
||||
let bob_msg = bob.recv_msg(&sent).await;
|
||||
assert_eq!(bob_msg.get_viewtype(), Viewtype::Image);
|
||||
assert_eq!(bob_msg.get_viewtype(), res_viewtype);
|
||||
assert_eq!(bob_msg.get_width() as u32, compressed_width);
|
||||
assert_eq!(bob_msg.get_height() as u32, compressed_height);
|
||||
let file_saved = bob
|
||||
@@ -564,7 +588,7 @@ impl SendImageCheckMediaquality<'_> {
|
||||
}
|
||||
|
||||
let (_, exif) = image_metadata(&std::fs::File::open(&file_saved)?)?;
|
||||
assert!(exif.is_none());
|
||||
assert!(res_viewtype != Viewtype::Image || exif.is_none());
|
||||
|
||||
let img = check_image_size(file_saved, compressed_width, compressed_height);
|
||||
|
||||
|
||||
686
src/calls.rs
Normal file
686
src/calls.rs
Normal file
@@ -0,0 +1,686 @@
|
||||
//! # Handle calls.
|
||||
//!
|
||||
//! Internally, calls are bound a user-visible message initializing the call.
|
||||
//! This means, the "Call ID" is a "Message ID" - similar to Webxdc IDs.
|
||||
use crate::chat::{Chat, ChatId, send_msg};
|
||||
use crate::constants::Chattype;
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::log::{info, warn};
|
||||
use crate::message::{self, Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::net::dns::lookup_host_with_cache;
|
||||
use crate::param::Param;
|
||||
use crate::tools::time;
|
||||
use anyhow::{Context as _, Result, ensure};
|
||||
use sdp::SessionDescription;
|
||||
use serde::Serialize;
|
||||
use std::io::Cursor;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use tokio::task;
|
||||
use tokio::time::sleep;
|
||||
|
||||
/// How long callee's or caller's phone ring.
|
||||
///
|
||||
/// For the callee, this is to prevent endless ringing
|
||||
/// in case the initial "call" is received, but then the caller went offline.
|
||||
/// Moreover, this prevents outdated calls to ring
|
||||
/// in case the initial "call" message arrives delayed.
|
||||
///
|
||||
/// For the caller, this means they should also not wait longer,
|
||||
/// as the callee won't start the call afterwards.
|
||||
const RINGING_SECONDS: i64 = 60;
|
||||
|
||||
// For persisting parameters in the call, we use Param::Arg*
|
||||
|
||||
const CALL_ACCEPTED_TIMESTAMP: Param = Param::Arg;
|
||||
const CALL_ENDED_TIMESTAMP: Param = Param::Arg4;
|
||||
|
||||
const STUN_PORT: u16 = 3478;
|
||||
|
||||
/// Set if incoming call was ended explicitly
|
||||
/// by the other side before we accepted it.
|
||||
///
|
||||
/// It is used to distinguish "ended" calls
|
||||
/// that are rejected by us from the calls
|
||||
/// canceled by the other side
|
||||
/// immediately after ringing started.
|
||||
const CALL_CANCELED_TIMESTAMP: Param = Param::Arg2;
|
||||
|
||||
/// Information about the status of a call.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct CallInfo {
|
||||
/// User-defined text as given to place_outgoing_call()
|
||||
pub place_call_info: String,
|
||||
|
||||
/// User-defined text as given to accept_incoming_call()
|
||||
pub accept_call_info: String,
|
||||
|
||||
/// Message referring to the call.
|
||||
/// Data are persisted along with the message using Param::Arg*
|
||||
pub msg: Message,
|
||||
}
|
||||
|
||||
impl CallInfo {
|
||||
/// Returns true if the call is an incoming call.
|
||||
pub fn is_incoming(&self) -> bool {
|
||||
self.msg.from_id != ContactId::SELF
|
||||
}
|
||||
|
||||
/// Returns true if the call should not ring anymore.
|
||||
pub fn is_stale(&self) -> bool {
|
||||
(self.is_incoming() || self.msg.timestamp_sent != 0) && self.remaining_ring_seconds() <= 0
|
||||
}
|
||||
|
||||
fn remaining_ring_seconds(&self) -> i64 {
|
||||
let remaining_seconds = self.msg.timestamp_sent + RINGING_SECONDS - time();
|
||||
remaining_seconds.clamp(0, RINGING_SECONDS)
|
||||
}
|
||||
|
||||
async fn update_text(&self, context: &Context, text: &str) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET txt=?, txt_normalized=? WHERE id=?",
|
||||
(text, message::normalize_text(text), self.msg.id),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_text_duration(&self, context: &Context) -> Result<()> {
|
||||
let minutes = self.duration_seconds() / 60;
|
||||
let duration = match minutes {
|
||||
0 => "<1 minute".to_string(),
|
||||
1 => "1 minute".to_string(),
|
||||
n => format!("{n} minutes"),
|
||||
};
|
||||
|
||||
if self.is_incoming() {
|
||||
self.update_text(context, &format!("Incoming call\n{duration}"))
|
||||
.await?;
|
||||
} else {
|
||||
self.update_text(context, &format!("Outgoing call\n{duration}"))
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark calls as accepted.
|
||||
/// This is needed for all devices where a stale-timer runs, to prevent accepted calls being terminated as stale.
|
||||
async fn mark_as_accepted(&mut self, context: &Context) -> Result<()> {
|
||||
self.msg.param.set_i64(CALL_ACCEPTED_TIMESTAMP, time());
|
||||
self.msg.update_param(context).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns true if the call is accepted.
|
||||
pub fn is_accepted(&self) -> bool {
|
||||
self.msg.param.exists(CALL_ACCEPTED_TIMESTAMP)
|
||||
}
|
||||
|
||||
/// Returns true if the call is missed
|
||||
/// because the caller canceled it
|
||||
/// explicitly before ringing stopped.
|
||||
///
|
||||
/// For outgoing calls this means
|
||||
/// the receiver has rejected the call
|
||||
/// explicitly.
|
||||
pub fn is_canceled(&self) -> bool {
|
||||
self.msg.param.exists(CALL_CANCELED_TIMESTAMP)
|
||||
}
|
||||
|
||||
async fn mark_as_ended(&mut self, context: &Context) -> Result<()> {
|
||||
self.msg.param.set_i64(CALL_ENDED_TIMESTAMP, time());
|
||||
self.msg.update_param(context).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Explicitly mark the call as canceled.
|
||||
///
|
||||
/// For incoming calls this should be called
|
||||
/// when "call ended" message is received
|
||||
/// from the caller before we picked up the call.
|
||||
/// In this case the call becomes "missed" early
|
||||
/// before the ringing timeout.
|
||||
async fn mark_as_canceled(&mut self, context: &Context) -> Result<()> {
|
||||
let now = time();
|
||||
self.msg.param.set_i64(CALL_ENDED_TIMESTAMP, now);
|
||||
self.msg.param.set_i64(CALL_CANCELED_TIMESTAMP, now);
|
||||
self.msg.update_param(context).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns true if the call is ended.
|
||||
pub fn is_ended(&self) -> bool {
|
||||
self.msg.param.exists(CALL_ENDED_TIMESTAMP)
|
||||
}
|
||||
|
||||
/// Returns call duration in seconds.
|
||||
pub fn duration_seconds(&self) -> i64 {
|
||||
if let (Some(start), Some(end)) = (
|
||||
self.msg.param.get_i64(CALL_ACCEPTED_TIMESTAMP),
|
||||
self.msg.param.get_i64(CALL_ENDED_TIMESTAMP),
|
||||
) {
|
||||
let seconds = end - start;
|
||||
if seconds <= 0 {
|
||||
return 1;
|
||||
}
|
||||
return seconds;
|
||||
}
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Start an outgoing call.
|
||||
pub async fn place_outgoing_call(
|
||||
&self,
|
||||
chat_id: ChatId,
|
||||
place_call_info: String,
|
||||
) -> Result<MsgId> {
|
||||
let chat = Chat::load_from_db(self, chat_id).await?;
|
||||
ensure!(
|
||||
chat.typ == Chattype::Single,
|
||||
"Can only place calls in 1:1 chats"
|
||||
);
|
||||
ensure!(!chat.is_self_talk(), "Cannot call self");
|
||||
|
||||
let mut call = Message {
|
||||
viewtype: Viewtype::Call,
|
||||
text: "Outgoing call".into(),
|
||||
..Default::default()
|
||||
};
|
||||
call.param.set(Param::WebrtcRoom, &place_call_info);
|
||||
call.id = send_msg(self, chat_id, &mut call).await?;
|
||||
|
||||
let wait = RINGING_SECONDS;
|
||||
task::spawn(Context::emit_end_call_if_unaccepted(
|
||||
self.clone(),
|
||||
wait.try_into()?,
|
||||
call.id,
|
||||
));
|
||||
|
||||
Ok(call.id)
|
||||
}
|
||||
|
||||
/// Accept an incoming call.
|
||||
pub async fn accept_incoming_call(
|
||||
&self,
|
||||
call_id: MsgId,
|
||||
accept_call_info: String,
|
||||
) -> Result<()> {
|
||||
let mut call: CallInfo = self.load_call_by_id(call_id).await?.with_context(|| {
|
||||
format!("accept_incoming_call is called with {call_id} which does not refer to a call")
|
||||
})?;
|
||||
ensure!(call.is_incoming());
|
||||
if call.is_accepted() || call.is_ended() {
|
||||
info!(self, "Call already accepted/ended");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
call.mark_as_accepted(self).await?;
|
||||
let chat = Chat::load_from_db(self, call.msg.chat_id).await?;
|
||||
if chat.is_contact_request() {
|
||||
chat.id.accept(self).await?;
|
||||
}
|
||||
|
||||
// send an acceptance message around: to the caller as well as to the other devices of the callee
|
||||
let mut msg = Message {
|
||||
viewtype: Viewtype::Text,
|
||||
text: "[Call accepted]".into(),
|
||||
..Default::default()
|
||||
};
|
||||
msg.param.set_cmd(SystemMessage::CallAccepted);
|
||||
msg.hidden = true;
|
||||
msg.param
|
||||
.set(Param::WebrtcAccepted, accept_call_info.to_string());
|
||||
msg.set_quote(self, Some(&call.msg)).await?;
|
||||
msg.id = send_msg(self, call.msg.chat_id, &mut msg).await?;
|
||||
self.emit_event(EventType::IncomingCallAccepted {
|
||||
msg_id: call.msg.id,
|
||||
chat_id: call.msg.chat_id,
|
||||
});
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cancel, decline or hangup an incoming or outgoing call.
|
||||
pub async fn end_call(&self, call_id: MsgId) -> Result<()> {
|
||||
let mut call: CallInfo = self.load_call_by_id(call_id).await?.with_context(|| {
|
||||
format!("end_call is called with {call_id} which does not refer to a call")
|
||||
})?;
|
||||
if call.is_ended() {
|
||||
info!(self, "Call already ended");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !call.is_accepted() {
|
||||
if call.is_incoming() {
|
||||
call.mark_as_ended(self).await?;
|
||||
call.update_text(self, "Declined call").await?;
|
||||
} else {
|
||||
call.mark_as_canceled(self).await?;
|
||||
call.update_text(self, "Canceled call").await?;
|
||||
}
|
||||
} else {
|
||||
call.mark_as_ended(self).await?;
|
||||
call.update_text_duration(self).await?;
|
||||
}
|
||||
|
||||
let mut msg = Message {
|
||||
viewtype: Viewtype::Text,
|
||||
text: "[Call ended]".into(),
|
||||
..Default::default()
|
||||
};
|
||||
msg.param.set_cmd(SystemMessage::CallEnded);
|
||||
msg.hidden = true;
|
||||
msg.set_quote(self, Some(&call.msg)).await?;
|
||||
msg.id = send_msg(self, call.msg.chat_id, &mut msg).await?;
|
||||
|
||||
self.emit_event(EventType::CallEnded {
|
||||
msg_id: call.msg.id,
|
||||
chat_id: call.msg.chat_id,
|
||||
});
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn emit_end_call_if_unaccepted(
|
||||
context: Context,
|
||||
wait: u64,
|
||||
call_id: MsgId,
|
||||
) -> Result<()> {
|
||||
sleep(Duration::from_secs(wait)).await;
|
||||
let Some(mut call) = context.load_call_by_id(call_id).await? else {
|
||||
warn!(
|
||||
context,
|
||||
"emit_end_call_if_unaccepted is called with {call_id} which does not refer to a call."
|
||||
);
|
||||
return Ok(());
|
||||
};
|
||||
if !call.is_accepted() && !call.is_ended() {
|
||||
if call.is_incoming() {
|
||||
call.mark_as_canceled(&context).await?;
|
||||
call.update_text(&context, "Missed call").await?;
|
||||
} else {
|
||||
call.mark_as_ended(&context).await?;
|
||||
call.update_text(&context, "Canceled call").await?;
|
||||
}
|
||||
context.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
context.emit_event(EventType::CallEnded {
|
||||
msg_id: call.msg.id,
|
||||
chat_id: call.msg.chat_id,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn handle_call_msg(
|
||||
&self,
|
||||
call_id: MsgId,
|
||||
mime_message: &MimeMessage,
|
||||
from_id: ContactId,
|
||||
) -> Result<()> {
|
||||
if mime_message.is_call() {
|
||||
let Some(call) = self.load_call_by_id(call_id).await? else {
|
||||
warn!(self, "{call_id} does not refer to a call message");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if call.is_incoming() {
|
||||
if call.is_stale() {
|
||||
call.update_text(self, "Missed call").await?;
|
||||
self.emit_incoming_msg(call.msg.chat_id, call_id); // notify missed call
|
||||
} else {
|
||||
call.update_text(self, "Incoming call").await?;
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id); // ringing calls are not additionally notified
|
||||
let has_video = match sdp_has_video(&call.place_call_info) {
|
||||
Ok(has_video) => has_video,
|
||||
Err(err) => {
|
||||
warn!(self, "Failed to determine if SDP offer has video: {err:#}.");
|
||||
false
|
||||
}
|
||||
};
|
||||
self.emit_event(EventType::IncomingCall {
|
||||
msg_id: call.msg.id,
|
||||
chat_id: call.msg.chat_id,
|
||||
place_call_info: call.place_call_info.to_string(),
|
||||
has_video,
|
||||
});
|
||||
let wait = call.remaining_ring_seconds();
|
||||
task::spawn(Context::emit_end_call_if_unaccepted(
|
||||
self.clone(),
|
||||
wait.try_into()?,
|
||||
call.msg.id,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
call.update_text(self, "Outgoing call").await?;
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
}
|
||||
} else {
|
||||
match mime_message.is_system_message {
|
||||
SystemMessage::CallAccepted => {
|
||||
let Some(mut call) = self.load_call_by_id(call_id).await? else {
|
||||
warn!(self, "{call_id} does not refer to a call message");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if call.is_ended() || call.is_accepted() {
|
||||
info!(self, "CallAccepted received for accepted/ended call");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
call.mark_as_accepted(self).await?;
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
if call.is_incoming() {
|
||||
self.emit_event(EventType::IncomingCallAccepted {
|
||||
msg_id: call.msg.id,
|
||||
chat_id: call.msg.chat_id,
|
||||
});
|
||||
} else {
|
||||
let accept_call_info = mime_message
|
||||
.get_header(HeaderDef::ChatWebrtcAccepted)
|
||||
.unwrap_or_default();
|
||||
self.emit_event(EventType::OutgoingCallAccepted {
|
||||
msg_id: call.msg.id,
|
||||
chat_id: call.msg.chat_id,
|
||||
accept_call_info: accept_call_info.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
SystemMessage::CallEnded => {
|
||||
let Some(mut call) = self.load_call_by_id(call_id).await? else {
|
||||
warn!(self, "{call_id} does not refer to a call message");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if call.is_ended() {
|
||||
// may happen eg. if a a message is missed
|
||||
info!(self, "CallEnded received for ended call");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !call.is_accepted() {
|
||||
if call.is_incoming() {
|
||||
if from_id == ContactId::SELF {
|
||||
call.mark_as_ended(self).await?;
|
||||
call.update_text(self, "Declined call").await?;
|
||||
} else {
|
||||
call.mark_as_canceled(self).await?;
|
||||
call.update_text(self, "Missed call").await?;
|
||||
}
|
||||
} else {
|
||||
// outgoing
|
||||
if from_id == ContactId::SELF {
|
||||
call.mark_as_canceled(self).await?;
|
||||
call.update_text(self, "Canceled call").await?;
|
||||
} else {
|
||||
call.mark_as_ended(self).await?;
|
||||
call.update_text(self, "Declined call").await?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
call.mark_as_ended(self).await?;
|
||||
call.update_text_duration(self).await?;
|
||||
}
|
||||
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
self.emit_event(EventType::CallEnded {
|
||||
msg_id: call.msg.id,
|
||||
chat_id: call.msg.chat_id,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Loads information about the call given its ID.
|
||||
///
|
||||
/// If the message referred to by ID is
|
||||
/// not a call message, returns `None`.
|
||||
pub async fn load_call_by_id(&self, call_id: MsgId) -> Result<Option<CallInfo>> {
|
||||
let call = Message::load_from_db(self, call_id).await?;
|
||||
Ok(self.load_call_by_message(call))
|
||||
}
|
||||
|
||||
// Loads information about the call given the `Message`.
|
||||
//
|
||||
// If the `Message` is not a call message, returns `None`
|
||||
fn load_call_by_message(&self, call: Message) -> Option<CallInfo> {
|
||||
if call.viewtype != Viewtype::Call {
|
||||
// This can happen e.g. if a "call accepted"
|
||||
// or "call ended" message is received
|
||||
// with `In-Reply-To` referring to non-call message.
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(CallInfo {
|
||||
place_call_info: call
|
||||
.param
|
||||
.get(Param::WebrtcRoom)
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
accept_call_info: call
|
||||
.param
|
||||
.get(Param::WebrtcAccepted)
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
msg: call,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if SDP offer has a video.
|
||||
pub fn sdp_has_video(sdp: &str) -> Result<bool> {
|
||||
let mut cursor = Cursor::new(sdp);
|
||||
let session_description =
|
||||
SessionDescription::unmarshal(&mut cursor).context("Failed to parse SDP")?;
|
||||
for media_description in &session_description.media_descriptions {
|
||||
if media_description.media_name.media == "video" {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// State of the call for display in the message bubble.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum CallState {
|
||||
/// Fresh incoming or outgoing call that is still ringing.
|
||||
///
|
||||
/// There is no separate state for outgoing call
|
||||
/// that has been dialled but not ringing on the other side yet
|
||||
/// as we don't know whether the other side received our call.
|
||||
Alerting,
|
||||
|
||||
/// Active call.
|
||||
Active,
|
||||
|
||||
/// Completed call that was once active
|
||||
/// and then was terminated for any reason.
|
||||
Completed {
|
||||
/// Call duration in seconds.
|
||||
duration: i64,
|
||||
},
|
||||
|
||||
/// Incoming call that was not picked up within a timeout
|
||||
/// or was explicitly ended by the caller before we picked up.
|
||||
Missed,
|
||||
|
||||
/// Incoming call that was explicitly ended on our side
|
||||
/// before picking up or outgoing call
|
||||
/// that was declined before the timeout.
|
||||
Declined,
|
||||
|
||||
/// Outgoing call that has been canceled on our side
|
||||
/// before receiving a response.
|
||||
///
|
||||
/// Incoming calls cannot be canceled,
|
||||
/// on the receiver side canceled calls
|
||||
/// usually result in missed calls.
|
||||
Canceled,
|
||||
}
|
||||
|
||||
/// Returns call state given the message ID.
|
||||
///
|
||||
/// Returns an error if the message is not a call message.
|
||||
pub async fn call_state(context: &Context, msg_id: MsgId) -> Result<CallState> {
|
||||
let call = context
|
||||
.load_call_by_id(msg_id)
|
||||
.await?
|
||||
.with_context(|| format!("{msg_id} is not a call message"))?;
|
||||
let state = if call.is_incoming() {
|
||||
if call.is_accepted() {
|
||||
if call.is_ended() {
|
||||
CallState::Completed {
|
||||
duration: call.duration_seconds(),
|
||||
}
|
||||
} else {
|
||||
CallState::Active
|
||||
}
|
||||
} else if call.is_canceled() {
|
||||
// Call was explicitly canceled
|
||||
// by the caller before we picked it up.
|
||||
CallState::Missed
|
||||
} else if call.is_ended() {
|
||||
CallState::Declined
|
||||
} else if call.is_stale() {
|
||||
CallState::Missed
|
||||
} else {
|
||||
CallState::Alerting
|
||||
}
|
||||
} else if call.is_accepted() {
|
||||
if call.is_ended() {
|
||||
CallState::Completed {
|
||||
duration: call.duration_seconds(),
|
||||
}
|
||||
} else {
|
||||
CallState::Active
|
||||
}
|
||||
} else if call.is_canceled() {
|
||||
CallState::Canceled
|
||||
} else if call.is_ended() || call.is_stale() {
|
||||
CallState::Declined
|
||||
} else {
|
||||
CallState::Alerting
|
||||
};
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
/// ICE server for JSON serialization.
|
||||
#[derive(Serialize, Debug, Clone, PartialEq)]
|
||||
struct IceServer {
|
||||
/// STUN or TURN URLs.
|
||||
pub urls: Vec<String>,
|
||||
|
||||
/// Username for TURN server authentication.
|
||||
pub username: Option<String>,
|
||||
|
||||
/// Password for logging into the server.
|
||||
pub credential: Option<String>,
|
||||
}
|
||||
|
||||
/// Creates JSON with ICE servers.
|
||||
async fn create_ice_servers(
|
||||
context: &Context,
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<String> {
|
||||
// Do not use cache because there is no TLS.
|
||||
let load_cache = false;
|
||||
let urls: Vec<String> = lookup_host_with_cache(context, hostname, port, "", load_cache)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|addr| format!("turn:{addr}"))
|
||||
.collect();
|
||||
|
||||
let ice_server = IceServer {
|
||||
urls,
|
||||
username: Some(username.to_string()),
|
||||
credential: Some(password.to_string()),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&[ice_server])?;
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
/// Creates JSON with ICE servers from a line received over IMAP METADATA.
|
||||
///
|
||||
/// IMAP METADATA returns a line such as
|
||||
/// `example.com:3478:1758650868:8Dqkyyu11MVESBqjbIylmB06rv8=`
|
||||
///
|
||||
/// 1758650868 is the username and expiration timestamp
|
||||
/// at the same time,
|
||||
/// while `8Dqkyyu11MVESBqjbIylmB06rv8=`
|
||||
/// is the password.
|
||||
pub(crate) async fn create_ice_servers_from_metadata(
|
||||
context: &Context,
|
||||
metadata: &str,
|
||||
) -> Result<(i64, String)> {
|
||||
let (hostname, rest) = metadata.split_once(':').context("Missing hostname")?;
|
||||
let (port, rest) = rest.split_once(':').context("Missing port")?;
|
||||
let port = u16::from_str(port).context("Failed to parse the port")?;
|
||||
let (ts, password) = rest.split_once(':').context("Missing timestamp")?;
|
||||
let expiration_timestamp = i64::from_str(ts).context("Failed to parse the timestamp")?;
|
||||
let ice_servers = create_ice_servers(context, hostname, port, ts, password).await?;
|
||||
Ok((expiration_timestamp, ice_servers))
|
||||
}
|
||||
|
||||
/// Creates JSON with ICE servers when no TURN servers are known.
|
||||
pub(crate) async fn create_fallback_ice_servers(context: &Context) -> Result<String> {
|
||||
// Do not use public STUN server from https://stunprotocol.org/.
|
||||
// It changes the hostname every year
|
||||
// (e.g. stunserver2025.stunprotocol.org
|
||||
// which was previously stunserver2024.stunprotocol.org)
|
||||
// because of bandwidth costs:
|
||||
// <https://github.com/jselbie/stunserver/issues/50>
|
||||
|
||||
// We use nine.testrun.org for a default STUN server.
|
||||
let hostname = "nine.testrun.org";
|
||||
|
||||
// Do not use cache because there is no TLS.
|
||||
let load_cache = false;
|
||||
let urls: Vec<String> = lookup_host_with_cache(context, hostname, STUN_PORT, "", load_cache)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|addr| format!("stun:{addr}"))
|
||||
.collect();
|
||||
|
||||
let ice_server = IceServer {
|
||||
urls,
|
||||
username: None,
|
||||
credential: None,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&[ice_server])?;
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
/// Returns JSON with ICE servers.
|
||||
///
|
||||
/// <https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection#iceservers>
|
||||
///
|
||||
/// All returned servers are resolved to their IP addresses.
|
||||
/// The primary point of DNS lookup is that Delta Chat Desktop
|
||||
/// relies on the servers being specified by IP,
|
||||
/// because it itself cannot utilize DNS. See
|
||||
/// <https://github.com/deltachat/deltachat-desktop/issues/5447>.
|
||||
pub async fn ice_servers(context: &Context) -> Result<String> {
|
||||
if let Some(ref metadata) = *context.metadata.read().await {
|
||||
Ok(metadata.ice_servers.clone())
|
||||
} else {
|
||||
Ok("[]".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod calls_tests;
|
||||
668
src/calls/calls_tests.rs
Normal file
668
src/calls/calls_tests.rs
Normal file
@@ -0,0 +1,668 @@
|
||||
use super::*;
|
||||
use crate::chat::forward_msgs;
|
||||
use crate::config::Config;
|
||||
use crate::constants::DC_CHAT_ID_TRASH;
|
||||
use crate::receive_imf::{receive_imf, receive_imf_from_inbox};
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
|
||||
struct CallSetup {
|
||||
pub alice: TestContext,
|
||||
pub alice2: TestContext,
|
||||
pub alice_call: Message,
|
||||
pub alice2_call: Message,
|
||||
pub bob: TestContext,
|
||||
pub bob2: TestContext,
|
||||
pub bob_call: Message,
|
||||
pub bob2_call: Message,
|
||||
}
|
||||
|
||||
async fn assert_text(t: &TestContext, call_id: MsgId, text: &str) -> Result<()> {
|
||||
assert_eq!(Message::load_from_db(t, call_id).await?.text, text);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Offer and answer examples from <https://www.rfc-editor.org/rfc/rfc3264>
|
||||
const PLACE_INFO: &str = "v=0\r\no=alice 2890844526 2890844526 IN IP4 host.anywhere.com\r\ns=-\r\nc=IN IP4 host.anywhere.com\r\nt=0 0\r\nm=audio 62986 RTP/AVP 0 4 18\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:4 G723/8000\r\na=rtpmap:18 G729/8000\r\na=inactive\r\n";
|
||||
const ACCEPT_INFO: &str = "v=0\r\no=bob 2890844730 2890844731 IN IP4 host.example.com\r\ns=\r\nc=IN IP4 host.example.com\r\nt=0 0\r\nm=audio 54344 RTP/AVP 0 4\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:4 G723/8000\r\na=inactive\r\n";
|
||||
|
||||
/// Example from <https://datatracker.ietf.org/doc/rfc9143/>
|
||||
/// with `s= ` replaced with `s=-`.
|
||||
///
|
||||
/// `s=` cannot be empty according to RFC 3264,
|
||||
/// so it is more clear as `s=-`.
|
||||
const PLACE_INFO_VIDEO: &str = "v=0\r\no=alice 2890844526 2890844526 IN IP6 2001:db8::3\r\ns=-\r\nc=IN IP6 2001:db8::3\r\nt=0 0\r\na=group:BUNDLE foo bar\r\n\r\nm=audio 10000 RTP/AVP 0 8 97\r\nb=AS:200\r\na=mid:foo\r\na=rtcp-mux\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:97 iLBC/8000\r\na=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r\n\r\nm=video 10002 RTP/AVP 31 32\r\nb=AS:1000\r\na=mid:bar\r\na=rtcp-mux\r\na=rtpmap:31 H261/90000\r\na=rtpmap:32 MPV/90000\r\na=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r\n";
|
||||
|
||||
async fn setup_call() -> Result<CallSetup> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let alice2 = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let bob2 = tcm.bob().await;
|
||||
for t in [&alice, &alice2, &bob, &bob2] {
|
||||
t.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
|
||||
// Alice creates a chat with Bob and places an outgoing call there.
|
||||
// Alice's other device sees the same message as an outgoing call.
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
let test_msg_id = alice
|
||||
.place_outgoing_call(alice_chat.id, PLACE_INFO.to_string())
|
||||
.await?;
|
||||
let sent1 = alice.pop_sent_msg().await;
|
||||
assert_eq!(sent1.sender_msg_id, test_msg_id);
|
||||
let alice_call = Message::load_from_db(&alice, sent1.sender_msg_id).await?;
|
||||
let alice2_call = alice2.recv_msg(&sent1).await;
|
||||
for (t, m) in [(&alice, &alice_call), (&alice2, &alice2_call)] {
|
||||
assert!(!m.is_info());
|
||||
assert_eq!(m.viewtype, Viewtype::Call);
|
||||
let info = t
|
||||
.load_call_by_id(m.id)
|
||||
.await?
|
||||
.expect("m should be a call message");
|
||||
assert!(!info.is_incoming());
|
||||
assert!(!info.is_accepted());
|
||||
assert_eq!(info.place_call_info, PLACE_INFO);
|
||||
assert_text(t, m.id, "Outgoing call").await?;
|
||||
assert_eq!(call_state(t, m.id).await?, CallState::Alerting);
|
||||
}
|
||||
|
||||
// Bob receives the message referring to the call on two devices;
|
||||
// it is an incoming call from the view of Bob
|
||||
let bob_call = bob.recv_msg(&sent1).await;
|
||||
let bob2_call = bob2.recv_msg(&sent1).await;
|
||||
for (t, m) in [(&bob, &bob_call), (&bob2, &bob2_call)] {
|
||||
assert!(!m.is_info());
|
||||
assert_eq!(m.viewtype, Viewtype::Call);
|
||||
t.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::IncomingCall { .. }))
|
||||
.await;
|
||||
let info = t
|
||||
.load_call_by_id(m.id)
|
||||
.await?
|
||||
.expect("IncomingCall event should refer to a call message");
|
||||
assert!(info.is_incoming());
|
||||
assert!(!info.is_accepted());
|
||||
assert_eq!(info.place_call_info, PLACE_INFO);
|
||||
assert_text(t, m.id, "Incoming call").await?;
|
||||
assert_eq!(call_state(t, m.id).await?, CallState::Alerting);
|
||||
}
|
||||
|
||||
Ok(CallSetup {
|
||||
alice,
|
||||
alice2,
|
||||
alice_call,
|
||||
alice2_call,
|
||||
bob,
|
||||
bob2,
|
||||
bob_call,
|
||||
bob2_call,
|
||||
})
|
||||
}
|
||||
|
||||
async fn accept_call() -> Result<CallSetup> {
|
||||
let CallSetup {
|
||||
alice,
|
||||
alice2,
|
||||
alice_call,
|
||||
alice2_call,
|
||||
bob,
|
||||
bob2,
|
||||
bob_call,
|
||||
bob2_call,
|
||||
} = setup_call().await?;
|
||||
|
||||
// Bob accepts the incoming call
|
||||
bob.accept_incoming_call(bob_call.id, ACCEPT_INFO.to_string())
|
||||
.await?;
|
||||
assert_text(&bob, bob_call.id, "Incoming call").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
|
||||
.await;
|
||||
let sent2 = bob.pop_sent_msg().await;
|
||||
let info = bob
|
||||
.load_call_by_id(bob_call.id)
|
||||
.await?
|
||||
.expect("bob_call should be a call message");
|
||||
assert!(info.is_accepted());
|
||||
assert_eq!(info.place_call_info, PLACE_INFO);
|
||||
assert_eq!(call_state(&bob, bob_call.id).await?, CallState::Active);
|
||||
|
||||
bob2.recv_msg_trash(&sent2).await;
|
||||
assert_text(&bob, bob_call.id, "Incoming call").await?;
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
|
||||
.await;
|
||||
let info = bob2
|
||||
.load_call_by_id(bob2_call.id)
|
||||
.await?
|
||||
.expect("bob2_call should be a call message");
|
||||
assert!(info.is_accepted());
|
||||
assert_eq!(call_state(&bob2, bob2_call.id).await?, CallState::Active);
|
||||
|
||||
// Alice receives the acceptance message
|
||||
alice.recv_msg_trash(&sent2).await;
|
||||
assert_text(&alice, alice_call.id, "Outgoing call").await?;
|
||||
let ev = alice
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::OutgoingCallAccepted { .. }))
|
||||
.await;
|
||||
assert_eq!(
|
||||
ev,
|
||||
EventType::OutgoingCallAccepted {
|
||||
msg_id: alice_call.id,
|
||||
chat_id: alice_call.chat_id,
|
||||
accept_call_info: ACCEPT_INFO.to_string()
|
||||
}
|
||||
);
|
||||
let info = alice
|
||||
.load_call_by_id(alice_call.id)
|
||||
.await?
|
||||
.expect("alice_call should be a call message");
|
||||
assert!(info.is_accepted());
|
||||
assert_eq!(info.place_call_info, PLACE_INFO);
|
||||
assert_eq!(call_state(&alice, alice_call.id).await?, CallState::Active);
|
||||
|
||||
alice2.recv_msg_trash(&sent2).await;
|
||||
assert_text(&alice2, alice2_call.id, "Outgoing call").await?;
|
||||
alice2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::OutgoingCallAccepted { .. }))
|
||||
.await;
|
||||
assert_eq!(
|
||||
call_state(&alice2, alice2_call.id).await?,
|
||||
CallState::Active
|
||||
);
|
||||
|
||||
Ok(CallSetup {
|
||||
alice,
|
||||
alice2,
|
||||
alice_call,
|
||||
alice2_call,
|
||||
bob,
|
||||
bob2,
|
||||
bob_call,
|
||||
bob2_call,
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_accept_call_callee_ends() -> Result<()> {
|
||||
// Alice calls Bob, Bob accepts
|
||||
let CallSetup {
|
||||
alice,
|
||||
alice_call,
|
||||
alice2,
|
||||
alice2_call,
|
||||
bob,
|
||||
bob2,
|
||||
bob_call,
|
||||
bob2_call,
|
||||
..
|
||||
} = accept_call().await?;
|
||||
|
||||
// Bob has accepted the call and also ends it
|
||||
bob.end_call(bob_call.id).await?;
|
||||
assert_text(&bob, bob_call.id, "Incoming call\n<1 minute").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
let sent3 = bob.pop_sent_msg().await;
|
||||
assert!(matches!(
|
||||
call_state(&bob, bob_call.id).await?,
|
||||
CallState::Completed { .. }
|
||||
));
|
||||
|
||||
bob2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob2, bob2_call.id, "Incoming call\n<1 minute").await?;
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
assert!(matches!(
|
||||
call_state(&bob2, bob2_call.id).await?,
|
||||
CallState::Completed { .. }
|
||||
));
|
||||
|
||||
// Alice receives the ending message
|
||||
alice.recv_msg_trash(&sent3).await;
|
||||
assert_text(&alice, alice_call.id, "Outgoing call\n<1 minute").await?;
|
||||
alice
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
assert!(matches!(
|
||||
call_state(&alice, alice_call.id).await?,
|
||||
CallState::Completed { .. }
|
||||
));
|
||||
|
||||
alice2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&alice2, alice2_call.id, "Outgoing call\n<1 minute").await?;
|
||||
alice2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
assert!(matches!(
|
||||
call_state(&alice2, alice2_call.id).await?,
|
||||
CallState::Completed { .. }
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_accept_call_caller_ends() -> Result<()> {
|
||||
// Alice calls Bob, Bob accepts
|
||||
let CallSetup {
|
||||
alice,
|
||||
alice_call,
|
||||
alice2,
|
||||
alice2_call,
|
||||
bob,
|
||||
bob2,
|
||||
bob_call,
|
||||
bob2_call,
|
||||
..
|
||||
} = accept_call().await?;
|
||||
|
||||
// Bob has accepted the call but Alice ends it
|
||||
alice.end_call(alice_call.id).await?;
|
||||
assert_text(&alice, alice_call.id, "Outgoing call\n<1 minute").await?;
|
||||
alice
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
let sent3 = alice.pop_sent_msg().await;
|
||||
assert!(matches!(
|
||||
call_state(&alice, alice_call.id).await?,
|
||||
CallState::Completed { .. }
|
||||
));
|
||||
|
||||
alice2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&alice2, alice2_call.id, "Outgoing call\n<1 minute").await?;
|
||||
alice2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
assert!(matches!(
|
||||
call_state(&alice2, alice2_call.id).await?,
|
||||
CallState::Completed { .. }
|
||||
));
|
||||
|
||||
// Bob receives the ending message
|
||||
bob.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob, bob_call.id, "Incoming call\n<1 minute").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
assert!(matches!(
|
||||
call_state(&bob, bob_call.id).await?,
|
||||
CallState::Completed { .. }
|
||||
));
|
||||
|
||||
bob2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob2, bob2_call.id, "Incoming call\n<1 minute").await?;
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
assert!(matches!(
|
||||
call_state(&bob2, bob2_call.id).await?,
|
||||
CallState::Completed { .. }
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_callee_rejects_call() -> Result<()> {
|
||||
// Alice calls Bob
|
||||
let CallSetup {
|
||||
alice,
|
||||
alice2,
|
||||
alice_call,
|
||||
alice2_call,
|
||||
bob,
|
||||
bob2,
|
||||
bob_call,
|
||||
bob2_call,
|
||||
..
|
||||
} = setup_call().await?;
|
||||
|
||||
// Bob has accepted Alice before, but does not want to talk with Alice
|
||||
bob_call.chat_id.accept(&bob).await?;
|
||||
bob.end_call(bob_call.id).await?;
|
||||
assert_text(&bob, bob_call.id, "Declined call").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
let sent3 = bob.pop_sent_msg().await;
|
||||
assert_eq!(call_state(&bob, bob_call.id).await?, CallState::Declined);
|
||||
|
||||
bob2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob2, bob2_call.id, "Declined call").await?;
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
assert_eq!(call_state(&bob2, bob2_call.id).await?, CallState::Declined);
|
||||
|
||||
// Alice receives decline message
|
||||
alice.recv_msg_trash(&sent3).await;
|
||||
assert_text(&alice, alice_call.id, "Declined call").await?;
|
||||
alice
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
assert_eq!(
|
||||
call_state(&alice, alice_call.id).await?,
|
||||
CallState::Declined
|
||||
);
|
||||
|
||||
alice2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&alice2, alice2_call.id, "Declined call").await?;
|
||||
alice2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
assert_eq!(
|
||||
call_state(&alice2, alice2_call.id).await?,
|
||||
CallState::Declined
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_caller_cancels_call() -> Result<()> {
|
||||
// Alice calls Bob
|
||||
let CallSetup {
|
||||
alice,
|
||||
alice2,
|
||||
alice_call,
|
||||
alice2_call,
|
||||
bob,
|
||||
bob2,
|
||||
bob_call,
|
||||
bob2_call,
|
||||
..
|
||||
} = setup_call().await?;
|
||||
|
||||
// Alice changes their mind before Bob picks up
|
||||
alice.end_call(alice_call.id).await?;
|
||||
assert_text(&alice, alice_call.id, "Canceled call").await?;
|
||||
alice
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
let sent3 = alice.pop_sent_msg().await;
|
||||
assert_eq!(
|
||||
call_state(&alice, alice_call.id).await?,
|
||||
CallState::Canceled
|
||||
);
|
||||
|
||||
alice2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&alice2, alice2_call.id, "Canceled call").await?;
|
||||
alice2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
assert_eq!(
|
||||
call_state(&alice2, alice2_call.id).await?,
|
||||
CallState::Canceled
|
||||
);
|
||||
|
||||
// Bob receives the ending message
|
||||
bob.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob, bob_call.id, "Missed call").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
assert_eq!(call_state(&bob, bob_call.id).await?, CallState::Missed);
|
||||
|
||||
// Test that message summary says it is a missed call.
|
||||
let bob_call_msg = Message::load_from_db(&bob, bob_call.id).await?;
|
||||
let summary = bob_call_msg.get_summary(&bob, None).await?;
|
||||
assert_eq!(summary.text, "📞 Missed call");
|
||||
|
||||
bob2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob2, bob2_call.id, "Missed call").await?;
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
assert_eq!(call_state(&bob2, bob2_call.id).await?, CallState::Missed);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_is_stale_call() -> Result<()> {
|
||||
// a call started now is not stale
|
||||
let call_info = CallInfo {
|
||||
msg: Message {
|
||||
timestamp_sent: time(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
assert!(!call_info.is_stale());
|
||||
let remaining_seconds = call_info.remaining_ring_seconds();
|
||||
assert!(remaining_seconds == RINGING_SECONDS || remaining_seconds == RINGING_SECONDS - 1);
|
||||
|
||||
// call started 5 seconds ago, this is not stale as well
|
||||
let call_info = CallInfo {
|
||||
msg: Message {
|
||||
timestamp_sent: time() - 5,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
assert!(!call_info.is_stale());
|
||||
let remaining_seconds = call_info.remaining_ring_seconds();
|
||||
assert!(remaining_seconds == RINGING_SECONDS - 5 || remaining_seconds == RINGING_SECONDS - 6);
|
||||
|
||||
// a call started one hour ago is clearly stale
|
||||
let call_info = CallInfo {
|
||||
msg: Message {
|
||||
timestamp_sent: time() - 3600,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
assert!(call_info.is_stale());
|
||||
assert_eq!(call_info.remaining_ring_seconds(), 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_mark_calls() -> Result<()> {
|
||||
let CallSetup {
|
||||
alice, alice_call, ..
|
||||
} = setup_call().await?;
|
||||
|
||||
let mut call_info: CallInfo = alice
|
||||
.load_call_by_id(alice_call.id)
|
||||
.await?
|
||||
.expect("alice_call should be a call message");
|
||||
assert!(!call_info.is_accepted());
|
||||
assert!(!call_info.is_ended());
|
||||
call_info.mark_as_accepted(&alice).await?;
|
||||
assert!(call_info.is_accepted());
|
||||
assert!(!call_info.is_ended());
|
||||
|
||||
let mut call_info: CallInfo = alice
|
||||
.load_call_by_id(alice_call.id)
|
||||
.await?
|
||||
.expect("alice_call should be a call message");
|
||||
assert!(call_info.is_accepted());
|
||||
assert!(!call_info.is_ended());
|
||||
|
||||
call_info.mark_as_ended(&alice).await?;
|
||||
assert!(call_info.is_accepted());
|
||||
assert!(call_info.is_ended());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_update_call_text() -> Result<()> {
|
||||
let CallSetup {
|
||||
alice, alice_call, ..
|
||||
} = setup_call().await?;
|
||||
|
||||
let call_info = alice
|
||||
.load_call_by_id(alice_call.id)
|
||||
.await?
|
||||
.expect("alice_call should be a call message");
|
||||
call_info.update_text(&alice, "foo bar").await?;
|
||||
|
||||
let alice_call = Message::load_from_db(&alice, alice_call.id).await?;
|
||||
assert_eq!(alice_call.get_text(), "foo bar");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sdp_has_video() {
|
||||
assert!(sdp_has_video("foobar").is_err());
|
||||
assert_eq!(sdp_has_video(PLACE_INFO).unwrap(), false);
|
||||
assert_eq!(sdp_has_video(PLACE_INFO_VIDEO).unwrap(), true);
|
||||
}
|
||||
|
||||
/// Tests that calls are forwarded as text messages.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_forward_call() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
|
||||
let alice_bob_chat = alice.create_chat(bob).await;
|
||||
let alice_msg_id = alice
|
||||
.place_outgoing_call(alice_bob_chat.id, PLACE_INFO.to_string())
|
||||
.await
|
||||
.context("Failed to place a call")?;
|
||||
let alice_call = Message::load_from_db(alice, alice_msg_id).await?;
|
||||
|
||||
let _alice_sent_call = alice.pop_sent_msg().await;
|
||||
assert_eq!(alice_call.viewtype, Viewtype::Call);
|
||||
|
||||
let alice_charlie_chat = alice.create_chat(charlie).await;
|
||||
forward_msgs(alice, &[alice_call.id], alice_charlie_chat.id).await?;
|
||||
let alice_forwarded_call = alice.pop_sent_msg().await;
|
||||
let alice_forwarded_call_msg = alice_forwarded_call.load_from_db().await;
|
||||
assert_eq!(alice_forwarded_call_msg.viewtype, Viewtype::Text);
|
||||
|
||||
let charlie_forwarded_call = charlie.recv_msg(&alice_forwarded_call).await;
|
||||
assert_eq!(charlie_forwarded_call.viewtype, Viewtype::Text);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that "end call" message referring
|
||||
/// to a text message does not make receive_imf fail.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_end_text_call() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
|
||||
let received1 = receive_imf(
|
||||
alice,
|
||||
b"From: bob@example.net\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <first@example.net>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
Chat-Version: 1.0\n\
|
||||
\n\
|
||||
Hello\n",
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_eq!(received1.msg_ids.len(), 1);
|
||||
let msg = Message::load_from_db(alice, received1.msg_ids[0])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msg.viewtype, Viewtype::Text);
|
||||
|
||||
// Receiving "Call ended" message that refers
|
||||
// to the text message does not result in an error.
|
||||
let received2 = receive_imf(
|
||||
alice,
|
||||
b"From: bob@example.net\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <second@example.net>\n\
|
||||
Date: Sun, 22 Mar 2020 23:37:57 +0000\n\
|
||||
In-Reply-To: <first@example.net>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Content: call-ended\n\
|
||||
\n\
|
||||
Call ended\n",
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_eq!(received2.msg_ids.len(), 1);
|
||||
assert_eq!(received2.chat_id, DC_CHAT_ID_TRASH);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that partially downloaded "call ended"
|
||||
/// messages are not processed.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_partial_calls() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
|
||||
let seen = false;
|
||||
|
||||
// The messages in the test
|
||||
// have no `Date` on purpose,
|
||||
// so they are treated as new.
|
||||
let received_call = receive_imf(
|
||||
alice,
|
||||
b"From: bob@example.net\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <first@example.net>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Content: call\n\
|
||||
Chat-Webrtc-Room: YWFhYWFhYWFhCg==\n\
|
||||
\n\
|
||||
Hello, this is a call\n",
|
||||
seen,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_eq!(received_call.msg_ids.len(), 1);
|
||||
let call_msg = Message::load_from_db(alice, received_call.msg_ids[0])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(call_msg.viewtype, Viewtype::Call);
|
||||
assert_eq!(call_state(alice, call_msg.id).await?, CallState::Alerting);
|
||||
|
||||
let imf_raw = b"From: bob@example.net\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <second@example.net>\n\
|
||||
In-Reply-To: <first@example.net>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Content: call-ended\n\
|
||||
\n\
|
||||
Call ended\n";
|
||||
receive_imf_from_inbox(
|
||||
alice,
|
||||
"second@example.net",
|
||||
imf_raw,
|
||||
seen,
|
||||
Some(imf_raw.len().try_into().unwrap()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// The call is still not ended.
|
||||
assert_eq!(call_state(alice, call_msg.id).await?, CallState::Alerting);
|
||||
|
||||
// Fully downloading the message ends the call.
|
||||
receive_imf_from_inbox(alice, "second@example.net", imf_raw, seen, None)
|
||||
.await
|
||||
.context("Failed to fully download end call message")?;
|
||||
assert_eq!(call_state(alice, call_msg.id).await?, CallState::Missed);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
456
src/chat.rs
456
src/chat.rs
@@ -33,6 +33,7 @@ use crate::ephemeral::{Timer as EphemeralTimer, start_chat_ephemeral_timers};
|
||||
use crate::events::EventType;
|
||||
use crate::location;
|
||||
use crate::log::{LogExt, error, info, warn};
|
||||
use crate::logged_debug_assert;
|
||||
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimefactory::MimeFactory;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
@@ -93,14 +94,12 @@ pub enum ProtectionStatus {
|
||||
///
|
||||
/// All members of the chat must be verified.
|
||||
Protected = 1,
|
||||
// `2` was never used as a value.
|
||||
|
||||
/// 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.
|
||||
// Chats don't break in Core v2 anymore. Chats with broken protection existing before the
|
||||
// key-contacts migration are treated as `Unprotected`.
|
||||
//
|
||||
// ProtectionBroken = 3,
|
||||
}
|
||||
|
||||
/// The reason why messages cannot be sent to the chat.
|
||||
@@ -117,10 +116,6 @@ pub(crate) enum CantSendReason {
|
||||
/// The chat is a contact request, it needs to be accepted before sending a message.
|
||||
ContactRequest,
|
||||
|
||||
/// 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,
|
||||
|
||||
@@ -143,10 +138,6 @@ 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")
|
||||
}
|
||||
@@ -348,6 +339,8 @@ impl ChatId {
|
||||
chat_id
|
||||
.add_protection_msg(context, ProtectionStatus::Protected, None, timestamp)
|
||||
.await?;
|
||||
} else {
|
||||
chat_id.maybe_add_encrypted_msg(context, timestamp).await?;
|
||||
}
|
||||
|
||||
info!(
|
||||
@@ -380,7 +373,7 @@ impl ChatId {
|
||||
/// Returns true if the value was modified.
|
||||
pub(crate) async fn set_blocked(self, context: &Context, new_blocked: Blocked) -> Result<bool> {
|
||||
if self.is_special() {
|
||||
bail!("ignoring setting of Block-status for {}", self);
|
||||
bail!("ignoring setting of Block-status for {self}");
|
||||
}
|
||||
let count = context
|
||||
.sql
|
||||
@@ -476,16 +469,6 @@ 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.
|
||||
//
|
||||
@@ -542,7 +525,7 @@ impl ChatId {
|
||||
| Chattype::InBroadcast => {}
|
||||
Chattype::Mailinglist => bail!("Cannot protect mailing lists"),
|
||||
},
|
||||
ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {}
|
||||
ProtectionStatus::Unprotected => {}
|
||||
};
|
||||
|
||||
context
|
||||
@@ -585,7 +568,6 @@ impl ChatId {
|
||||
let cmd = match protect {
|
||||
ProtectionStatus::Protected => SystemMessage::ChatProtectionEnabled,
|
||||
ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled,
|
||||
ProtectionStatus::ProtectionBroken => SystemMessage::ChatProtectionDisabled,
|
||||
};
|
||||
add_info_msg_with_cmd(
|
||||
context,
|
||||
@@ -603,6 +585,42 @@ impl ChatId {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds message "Messages are end-to-end encrypted" if appropriate.
|
||||
///
|
||||
/// This function is rather slow because it does a lot of database queries,
|
||||
/// but this is fine because it is only called on chat creation.
|
||||
async fn maybe_add_encrypted_msg(self, context: &Context, timestamp_sort: i64) -> Result<()> {
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
|
||||
// as secure-join adds its own message on success (after some other messasges),
|
||||
// we do not want to add "Messages are end-to-end encrypted" on chat creation.
|
||||
// we detect secure join by `can_send` (for Bob, scanner side) and by `blocked` (for Alice, inviter side) below.
|
||||
if !chat.is_encrypted(context).await?
|
||||
|| self <= DC_CHAT_ID_LAST_SPECIAL
|
||||
|| chat.is_device_talk()
|
||||
|| chat.is_self_talk()
|
||||
|| (!chat.can_send(context).await? && !chat.is_contact_request())
|
||||
|| chat.blocked == Blocked::Yes
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let text = stock_str::messages_e2e_encrypted(context).await;
|
||||
add_info_msg_with_cmd(
|
||||
context,
|
||||
self,
|
||||
&text,
|
||||
SystemMessage::ChatE2ee,
|
||||
timestamp_sort,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets protection and adds a message.
|
||||
///
|
||||
/// `timestamp_sort` is used as the timestamp of the added message
|
||||
@@ -684,8 +702,7 @@ impl ChatId {
|
||||
) -> Result<()> {
|
||||
ensure!(
|
||||
!self.is_special(),
|
||||
"bad chat_id, can not be special chat: {}",
|
||||
self
|
||||
"bad chat_id, can not be special chat: {self}"
|
||||
);
|
||||
|
||||
context
|
||||
@@ -795,8 +812,7 @@ impl ChatId {
|
||||
pub(crate) async fn delete_ex(self, context: &Context, sync: sync::Sync) -> Result<()> {
|
||||
ensure!(
|
||||
!self.is_special(),
|
||||
"bad chat_id, can not be a special chat: {}",
|
||||
self
|
||||
"bad chat_id, can not be a special chat: {self}"
|
||||
);
|
||||
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
@@ -1339,14 +1355,18 @@ impl ChatId {
|
||||
|
||||
let mut ret = stock_str::e2e_available(context).await + "\n";
|
||||
|
||||
for contact_id in get_chat_contacts(context, self)
|
||||
for &contact_id in get_chat_contacts(context, self)
|
||||
.await?
|
||||
.iter()
|
||||
.filter(|&contact_id| !contact_id.is_special())
|
||||
{
|
||||
let contact = Contact::get_by_id(context, *contact_id).await?;
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
let addr = contact.get_addr();
|
||||
debug_assert!(contact.is_key_contact());
|
||||
logged_debug_assert!(
|
||||
context,
|
||||
contact.is_key_contact(),
|
||||
"get_encryption_info: contact {contact_id} is not a key-contact."
|
||||
);
|
||||
let fingerprint = contact
|
||||
.fingerprint()
|
||||
.context("Contact does not have a fingerprint in encrypted chat")?;
|
||||
@@ -1657,12 +1677,6 @@ 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) {
|
||||
@@ -1755,6 +1769,12 @@ 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
|
||||
@@ -1764,12 +1784,6 @@ 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() {
|
||||
@@ -1781,8 +1795,9 @@ impl Chat {
|
||||
|
||||
/// Returns chat avatar color.
|
||||
///
|
||||
/// For 1:1 chats, the color is calculated from the contact's address.
|
||||
/// For group chats the color is calculated from the chat name.
|
||||
/// 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.
|
||||
pub async fn get_color(&self, context: &Context) -> Result<u32> {
|
||||
let mut color = 0;
|
||||
|
||||
@@ -1793,6 +1808,8 @@ 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);
|
||||
}
|
||||
@@ -1870,16 +1887,25 @@ impl Chat {
|
||||
let is_encrypted = self.is_protected()
|
||||
|| match self.typ {
|
||||
Chattype::Single => {
|
||||
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
|
||||
match context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT cc.contact_id, c.fingerprint<>''
|
||||
FROM chats_contacts cc LEFT JOIN contacts c
|
||||
ON c.id=cc.contact_id
|
||||
WHERE cc.chat_id=?
|
||||
",
|
||||
(self.id,),
|
||||
|row| {
|
||||
let id: ContactId = row.get(0)?;
|
||||
let is_key: bool = row.get(1)?;
|
||||
Ok((id, is_key))
|
||||
},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some((id, is_key)) => is_key || id == ContactId::DEVICE,
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
Chattype::Group => {
|
||||
@@ -1892,27 +1918,6 @@ impl Chat {
|
||||
Ok(is_encrypted)
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
match self.protected {
|
||||
ProtectionStatus::Protected => false,
|
||||
ProtectionStatus::Unprotected => false,
|
||||
ProtectionStatus::ProtectionBroken => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if location streaming is enabled in the chat.
|
||||
pub fn is_sending_locations(&self) -> bool {
|
||||
self.is_sending_locations
|
||||
@@ -1950,7 +1955,7 @@ impl Chat {
|
||||
}
|
||||
|
||||
/// Adds missing values to the msg object,
|
||||
/// writes the record to the database and returns its msg_id.
|
||||
/// writes the record to the database.
|
||||
///
|
||||
/// If `update_msg_id` is set, that record is reused;
|
||||
/// if `update_msg_id` is None, a new record is created.
|
||||
@@ -1959,7 +1964,7 @@ impl Chat {
|
||||
context: &Context,
|
||||
msg: &mut Message,
|
||||
update_msg_id: Option<MsgId>,
|
||||
) -> Result<MsgId> {
|
||||
) -> Result<()> {
|
||||
let mut to_id = 0;
|
||||
let mut location_id = 0;
|
||||
|
||||
@@ -2237,7 +2242,7 @@ impl Chat {
|
||||
.await?;
|
||||
}
|
||||
context.scheduler.interrupt_ephemeral_task().await;
|
||||
Ok(msg.id)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends a `SyncAction` synchronising chat contacts to other devices.
|
||||
@@ -2490,11 +2495,13 @@ pub(crate) async fn get_archive_icon(context: &Context) -> Result<PathBuf> {
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_address_contact_icon(context: &Context) -> Result<PathBuf> {
|
||||
/// Returns path to the icon
|
||||
/// indicating unencrypted chats and address-contacts.
|
||||
pub(crate) async fn get_unencrypted_icon(context: &Context) -> Result<PathBuf> {
|
||||
get_asset_icon(
|
||||
context,
|
||||
"icon-address-contact",
|
||||
include_bytes!("../assets/icon-address-contact.png"),
|
||||
"icon-unencrypted",
|
||||
include_bytes!("../assets/icon-unencrypted.png"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -2668,6 +2675,10 @@ impl ChatIdBlocked {
|
||||
smeared_time,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
chat_id
|
||||
.maybe_add_encrypted_msg(context, smeared_time)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
@@ -2678,7 +2689,7 @@ impl ChatIdBlocked {
|
||||
}
|
||||
|
||||
async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
if msg.viewtype == Viewtype::Text || msg.viewtype == Viewtype::VideochatInvitation {
|
||||
if msg.viewtype == Viewtype::Text || msg.viewtype == Viewtype::Call {
|
||||
// the caller should check if the message text is empty
|
||||
} else if msg.viewtype.has_file() {
|
||||
let viewtype_orig = msg.viewtype;
|
||||
@@ -2900,15 +2911,17 @@ async fn prepare_send_msg(
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
|
||||
let skip_fn = |reason: &CantSendReason| match reason {
|
||||
CantSendReason::ProtectionBroken | CantSendReason::ContactRequest => {
|
||||
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
|
||||
}
|
||||
// Allow to send "Member removed" messages so we can leave the group.
|
||||
// Allow to send "Member removed" messages so we can leave the group/broadcast.
|
||||
// Necessary checks should be made anyway before removing contact
|
||||
// from the chat.
|
||||
CantSendReason::NotAMember => msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup,
|
||||
CantSendReason::NotAMember | CantSendReason::InBroadcast => {
|
||||
msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup
|
||||
}
|
||||
CantSendReason::MissingKey => msg
|
||||
.param
|
||||
.get_bool(Param::ForcePlaintext)
|
||||
@@ -2954,17 +2967,23 @@ async fn prepare_send_msg(
|
||||
if !msg.hidden {
|
||||
chat_id.unarchive_if_not_muted(context, msg.state).await?;
|
||||
}
|
||||
msg.id = chat.prepare_msg_raw(context, msg, update_msg_id).await?;
|
||||
msg.chat_id = chat_id;
|
||||
chat.prepare_msg_raw(context, msg, update_msg_id).await?;
|
||||
|
||||
let row_ids = create_send_msg_jobs(context, msg)
|
||||
.await
|
||||
.context("Failed to create send jobs")?;
|
||||
if !row_ids.is_empty() {
|
||||
donation_request_maybe(context).await.log_err(context).ok();
|
||||
}
|
||||
Ok(row_ids)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -2976,7 +2995,16 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
}
|
||||
|
||||
let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default();
|
||||
let mimefactory = MimeFactory::from_msg(context, msg.clone()).await?;
|
||||
let mimefactory = match MimeFactory::from_msg(context, msg.clone()).await {
|
||||
Ok(mf) => mf,
|
||||
Err(err) => {
|
||||
// Mark message as failed
|
||||
message::set_msg_failed(context, msg, &err.to_string())
|
||||
.await
|
||||
.ok();
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
let attach_selfavatar = mimefactory.attach_selfavatar;
|
||||
let mut recipients = mimefactory.recipients();
|
||||
|
||||
@@ -3058,13 +3086,20 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
}
|
||||
}
|
||||
|
||||
if rendered_msg.is_encrypted && !needs_encryption {
|
||||
if rendered_msg.is_encrypted {
|
||||
msg.param.set_int(Param::GuaranteeE2ee, 1);
|
||||
msg.update_param(context).await?;
|
||||
} else {
|
||||
msg.param.remove(Param::GuaranteeE2ee);
|
||||
}
|
||||
|
||||
msg.subject.clone_from(&rendered_msg.subject);
|
||||
msg.update_subject(context).await?;
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET subject=?, param=? WHERE id=?",
|
||||
(&msg.subject, msg.param.to_string(), msg.id),
|
||||
)
|
||||
.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();
|
||||
@@ -3108,8 +3143,7 @@ pub async fn send_text_msg(
|
||||
) -> Result<MsgId> {
|
||||
ensure!(
|
||||
!chat_id.is_special(),
|
||||
"bad chat_id, can not be a special chat: {}",
|
||||
chat_id
|
||||
"bad chat_id, can not be a special chat: {chat_id}"
|
||||
);
|
||||
|
||||
let mut msg = Message::new_text(text_to_send);
|
||||
@@ -3125,10 +3159,7 @@ pub async fn send_edit_request(context: &Context, msg_id: MsgId, new_text: Strin
|
||||
);
|
||||
ensure!(!original_msg.is_info(), "Cannot edit info messages");
|
||||
ensure!(!original_msg.has_html(), "Cannot edit HTML messages");
|
||||
ensure!(
|
||||
original_msg.viewtype != Viewtype::VideochatInvitation,
|
||||
"Cannot edit videochat invitations"
|
||||
);
|
||||
ensure!(original_msg.viewtype != Viewtype::Call, "Cannot edit calls");
|
||||
ensure!(
|
||||
!original_msg.text.is_empty(), // avoid complexity in UI element changes. focus is typos and rewordings
|
||||
"Cannot add text"
|
||||
@@ -3176,32 +3207,29 @@ pub(crate) async fn save_text_edit_to_db(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends invitation to a videochat.
|
||||
pub async fn send_videochat_invitation(context: &Context, chat_id: ChatId) -> Result<MsgId> {
|
||||
ensure!(
|
||||
!chat_id.is_special(),
|
||||
"video chat invitation cannot be sent to special chat: {}",
|
||||
chat_id
|
||||
async fn donation_request_maybe(context: &Context) -> Result<()> {
|
||||
let secs_between_checks = 30 * 24 * 60 * 60;
|
||||
let now = time();
|
||||
let ts = context
|
||||
.get_config_i64(Config::DonationRequestNextCheck)
|
||||
.await?;
|
||||
if ts > now {
|
||||
return Ok(());
|
||||
}
|
||||
let msg_cnt = context.sql.count(
|
||||
"SELECT COUNT(*) FROM msgs WHERE state>=? AND hidden=0",
|
||||
(MessageState::OutDelivered,),
|
||||
);
|
||||
|
||||
let instance = if let Some(instance) = context.get_config(Config::WebrtcInstance).await? {
|
||||
if !instance.is_empty() {
|
||||
instance
|
||||
} else {
|
||||
bail!("webrtc_instance is empty");
|
||||
}
|
||||
let ts = if ts == 0 || msg_cnt.await? < 100 {
|
||||
now.saturating_add(secs_between_checks)
|
||||
} else {
|
||||
bail!("webrtc_instance not set");
|
||||
let mut msg = Message::new_text(stock_str::donation_request(context).await);
|
||||
add_device_msg(context, None, Some(&mut msg)).await?;
|
||||
i64::MAX
|
||||
};
|
||||
|
||||
let instance = Message::create_webrtc_instance(&instance, &create_id());
|
||||
|
||||
let mut msg = Message::new(Viewtype::VideochatInvitation);
|
||||
msg.param.set(Param::WebrtcRoom, &instance);
|
||||
msg.text =
|
||||
stock_str::videochat_invite_msg_body(context, &Message::parse_webrtc_instance(&instance).1)
|
||||
.await;
|
||||
send_msg(context, chat_id, &mut msg).await
|
||||
context
|
||||
.set_config_internal(Config::DonationRequestNextCheck, Some(&ts.to_string()))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Chat message list request options.
|
||||
@@ -3289,10 +3317,11 @@ 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 curr_day = curr_local_timestamp / 86400;
|
||||
let secs_in_day = 86400;
|
||||
let curr_day = curr_local_timestamp / secs_in_day;
|
||||
if curr_day != last_day {
|
||||
ret.push(ChatItem::DayMarker {
|
||||
timestamp: curr_day * 86400, // Convert day back to Unix timestamp
|
||||
timestamp: curr_day * secs_in_day - cnv_to_local,
|
||||
});
|
||||
last_day = curr_day;
|
||||
}
|
||||
@@ -3622,15 +3651,36 @@ pub async fn get_past_chat_contacts(context: &Context, chat_id: ChatId) -> Resul
|
||||
}
|
||||
|
||||
/// Creates a group chat with a given `name`.
|
||||
/// Deprecated on 2025-06-21, use `create_group_ex()`.
|
||||
pub async fn create_group_chat(
|
||||
context: &Context,
|
||||
protect: ProtectionStatus,
|
||||
chat_name: &str,
|
||||
name: &str,
|
||||
) -> Result<ChatId> {
|
||||
let chat_name = sanitize_single_line(chat_name);
|
||||
ensure!(!chat_name.is_empty(), "Invalid chat name");
|
||||
create_group_ex(context, Some(protect), name).await
|
||||
}
|
||||
|
||||
let grpid = create_id();
|
||||
/// Creates a group chat.
|
||||
///
|
||||
/// * `encryption` - If `Some`, the chat is encrypted (with key-contacts) and can be protected.
|
||||
/// * `name` - Chat name.
|
||||
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 grpid = match encryption {
|
||||
Some(_) => create_id(),
|
||||
None => String::new(),
|
||||
};
|
||||
|
||||
let timestamp = create_smeared_timestamp(context);
|
||||
let row_id = context
|
||||
@@ -3650,10 +3700,19 @@ pub async fn create_group_chat(
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
|
||||
if protect == ProtectionStatus::Protected {
|
||||
chat_id
|
||||
.set_protection_for_timestamp_sort(context, protect, timestamp, None)
|
||||
.await?;
|
||||
match encryption {
|
||||
Some(ProtectionStatus::Protected) => {
|
||||
let protect = ProtectionStatus::Protected;
|
||||
chat_id
|
||||
.set_protection_for_timestamp_sort(context, protect, timestamp, None)
|
||||
.await?;
|
||||
}
|
||||
Some(ProtectionStatus::Unprotected) => {
|
||||
// Add "Messages are end-to-end encrypted." message
|
||||
// even to unprotected chats.
|
||||
chat_id.maybe_add_encrypted_msg(context, timestamp).await?;
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
if !context.get_config_bool(Config::Bot).await?
|
||||
@@ -3852,13 +3911,11 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
ensure!(
|
||||
chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast,
|
||||
"{} is not a group/broadcast where one can add members",
|
||||
chat_id
|
||||
"{chat_id} is not a group/broadcast where one can add members"
|
||||
);
|
||||
ensure!(
|
||||
Contact::real_exists_by_id(context, contact_id).await? || contact_id == ContactId::SELF,
|
||||
"invalid contact_id {} for adding to group",
|
||||
contact_id
|
||||
"invalid contact_id {contact_id} for adding to group"
|
||||
);
|
||||
ensure!(!chat.is_mailing_list(), "Mailing lists can't be changed");
|
||||
ensure!(
|
||||
@@ -4071,16 +4128,13 @@ pub async fn remove_contact_from_chat(
|
||||
) -> Result<()> {
|
||||
ensure!(
|
||||
!chat_id.is_special(),
|
||||
"bad chat_id, can not be special chat: {}",
|
||||
chat_id
|
||||
"bad chat_id, can not be special chat: {chat_id}"
|
||||
);
|
||||
ensure!(
|
||||
!contact_id.is_special() || contact_id == ContactId::SELF,
|
||||
"Cannot remove special contact"
|
||||
);
|
||||
|
||||
let mut msg = Message::new(Viewtype::default());
|
||||
|
||||
let chat = Chat::load_from_db(context, chat_id).await?;
|
||||
if chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast {
|
||||
if !chat.is_self_in_chat(context).await? {
|
||||
@@ -4088,7 +4142,7 @@ pub async fn remove_contact_from_chat(
|
||||
"Cannot remove contact {contact_id} from chat {chat_id}: self not in group."
|
||||
);
|
||||
context.emit_event(EventType::ErrorSelfNotInGroup(err_msg.clone()));
|
||||
bail!("{}", err_msg);
|
||||
bail!("{err_msg}");
|
||||
} else {
|
||||
let mut sync = Nosync;
|
||||
|
||||
@@ -4110,19 +4164,10 @@ pub async fn remove_contact_from_chat(
|
||||
// in case of the database becoming inconsistent due to a bug.
|
||||
if let Some(contact) = Contact::get_by_id_optional(context, contact_id).await? {
|
||||
if chat.typ == Chattype::Group && chat.is_promoted() {
|
||||
msg.viewtype = Viewtype::Text;
|
||||
if contact_id == ContactId::SELF {
|
||||
msg.text = stock_str::msg_group_left_local(context, ContactId::SELF).await;
|
||||
} else {
|
||||
msg.text =
|
||||
stock_str::msg_del_member_local(context, contact_id, ContactId::SELF)
|
||||
.await;
|
||||
}
|
||||
msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
|
||||
msg.param.set(Param::Arg, contact.get_addr().to_lowercase());
|
||||
msg.param
|
||||
.set(Param::ContactAddedRemoved, contact.id.to_u32() as i32);
|
||||
let res = send_msg(context, chat_id, &mut msg).await;
|
||||
let addr = contact.get_addr();
|
||||
|
||||
let res = send_member_removal_msg(context, &chat, contact_id, addr).await;
|
||||
|
||||
if contact_id == ContactId::SELF {
|
||||
res?;
|
||||
set_group_explicitly_left(context, &chat.grpid).await?;
|
||||
@@ -4141,6 +4186,11 @@ pub async fn remove_contact_from_chat(
|
||||
chat.sync_contacts(context).await.log_err(context).ok();
|
||||
}
|
||||
}
|
||||
} else if chat.typ == Chattype::InBroadcast && contact_id == ContactId::SELF {
|
||||
// For incoming broadcast channels, it's not possible to remove members,
|
||||
// but it's possible to leave:
|
||||
let self_addr = context.get_primary_self_addr().await?;
|
||||
send_member_removal_msg(context, &chat, contact_id, &self_addr).await?;
|
||||
} else {
|
||||
bail!("Cannot remove members from non-group chats.");
|
||||
}
|
||||
@@ -4148,6 +4198,32 @@ pub async fn remove_contact_from_chat(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_member_removal_msg(
|
||||
context: &Context,
|
||||
chat: &Chat,
|
||||
contact_id: ContactId,
|
||||
addr: &str,
|
||||
) -> Result<MsgId> {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
|
||||
if contact_id == ContactId::SELF {
|
||||
if chat.typ == Chattype::InBroadcast {
|
||||
msg.text = stock_str::msg_you_left_broadcast(context).await;
|
||||
} else {
|
||||
msg.text = stock_str::msg_group_left_local(context, ContactId::SELF).await;
|
||||
}
|
||||
} else {
|
||||
msg.text = stock_str::msg_del_member_local(context, contact_id, ContactId::SELF).await;
|
||||
}
|
||||
|
||||
msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
|
||||
msg.param.set(Param::Arg, addr.to_lowercase());
|
||||
msg.param
|
||||
.set(Param::ContactAddedRemoved, contact_id.to_u32());
|
||||
|
||||
send_msg(context, chat.id, &mut msg).await
|
||||
}
|
||||
|
||||
async fn set_group_explicitly_left(context: &Context, grpid: &str) -> Result<()> {
|
||||
if !is_group_explicitly_left(context, grpid).await? {
|
||||
context
|
||||
@@ -4286,7 +4362,7 @@ pub async fn set_chat_profile_image(
|
||||
msg.text = stock_str::msg_grp_img_changed(context, ContactId::SELF).await;
|
||||
}
|
||||
chat.update_param(context).await?;
|
||||
if chat.is_promoted() && !chat.is_mailing_list() {
|
||||
if chat.is_promoted() {
|
||||
msg.id = send_msg(context, chat_id, &mut msg).await?;
|
||||
context.emit_msgs_changed(chat_id, msg.id);
|
||||
}
|
||||
@@ -4308,7 +4384,7 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
.await?;
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
if let Some(reason) = chat.why_cant_send(context).await? {
|
||||
bail!("cannot send to {}: {}", chat_id, reason);
|
||||
bail!("cannot send to {chat_id}: {reason}");
|
||||
}
|
||||
curr_timestamp = create_smeared_timestamps(context, msg_ids.len());
|
||||
let mut msgs = Vec::with_capacity(msg_ids.len());
|
||||
@@ -4333,6 +4409,10 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
.set_int(Param::Forwarded, src_msg_id.to_u32() as i32);
|
||||
}
|
||||
|
||||
if msg.get_viewtype() == Viewtype::Call {
|
||||
msg.viewtype = Viewtype::Text;
|
||||
}
|
||||
|
||||
msg.param.remove(Param::GuaranteeE2ee);
|
||||
msg.param.remove(Param::ForcePlaintext);
|
||||
msg.param.remove(Param::Cmd);
|
||||
@@ -4342,6 +4422,8 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
msg.param.remove(Param::WebxdcSummary);
|
||||
msg.param.remove(Param::WebxdcSummaryTimestamp);
|
||||
msg.param.remove(Param::IsEdited);
|
||||
msg.param.remove(Param::WebrtcRoom);
|
||||
msg.param.remove(Param::WebrtcAccepted);
|
||||
msg.in_reply_to = None;
|
||||
|
||||
// do not leak data as group names; a default subject is generated by mimefactory
|
||||
@@ -4350,13 +4432,13 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
msg.state = MessageState::OutPending;
|
||||
msg.rfc724_mid = create_outgoing_rfc724_mid();
|
||||
msg.timestamp_sort = curr_timestamp;
|
||||
let new_msg_id = chat.prepare_msg_raw(context, &mut msg, None).await?;
|
||||
chat.prepare_msg_raw(context, &mut msg, None).await?;
|
||||
|
||||
curr_timestamp += 1;
|
||||
if !create_send_msg_jobs(context, &mut msg).await?.is_empty() {
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
}
|
||||
created_msgs.push(new_msg_id);
|
||||
created_msgs.push(msg.id);
|
||||
}
|
||||
for msg_id in created_msgs {
|
||||
context.emit_msgs_changed(chat_id, msg_id);
|
||||
@@ -4413,15 +4495,24 @@ pub(crate) async fn save_copy_in_self_talk(
|
||||
bail!("message already saved.");
|
||||
}
|
||||
|
||||
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 copy_fields = "from_id, to_id, 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}, chat_id, rfc724_mid, state, timestamp, param, starred) \
|
||||
SELECT {copy_fields}, ?, ?, ?, ?, ?, ? \
|
||||
FROM msgs WHERE id=?;"
|
||||
"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=?;"
|
||||
),
|
||||
(
|
||||
dest_chat_id,
|
||||
@@ -4452,18 +4543,9 @@ 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"
|
||||
@@ -4472,16 +4554,7 @@ 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
|
||||
@@ -4492,16 +4565,21 @@ 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(
|
||||
|
||||
@@ -5,13 +5,15 @@ use crate::ephemeral::Timer;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::imex::{ImexMode, has_backup, imex};
|
||||
use crate::message::{MessengerMessage, delete_msgs};
|
||||
use crate::mimeparser;
|
||||
use crate::mimeparser::{self, MimeMessage};
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{
|
||||
AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext, TestContextManager,
|
||||
AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, E2EE_INFO_MSGS, TestContext, TestContextManager,
|
||||
TimeShiftFalsePositiveNote, sync,
|
||||
};
|
||||
use crate::tools::SystemTime;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::time::Duration;
|
||||
use strum::IntoEnumIterator;
|
||||
use tokio::fs;
|
||||
|
||||
@@ -32,7 +34,7 @@ async fn test_chat_info() {
|
||||
"archived": false,
|
||||
"param": "",
|
||||
"is_sending_locations": false,
|
||||
"color": 35391,
|
||||
"color": 29381,
|
||||
"profile_image": {},
|
||||
"draft": "",
|
||||
"is_muted": false,
|
||||
@@ -374,7 +376,10 @@ async fn test_member_add_remove() -> Result<()> {
|
||||
// Alice leaves the chat.
|
||||
remove_contact_from_chat(&alice, alice_chat_id, ContactId::SELF).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
assert_eq!(sent.load_from_db().await.get_text(), "You left the group.");
|
||||
assert_eq!(
|
||||
sent.load_from_db().await.get_text(),
|
||||
stock_str::msg_group_left_local(&alice, ContactId::SELF).await
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1641,7 +1646,7 @@ async fn test_set_mute_duration() {
|
||||
async fn test_add_info_msg() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
||||
add_info_msg(&t, chat_id, "foo info", 200000).await?;
|
||||
add_info_msg(&t, chat_id, "foo info", time()).await?;
|
||||
|
||||
let msg = t.get_last_msg_in(chat_id).await;
|
||||
assert_eq!(msg.get_chat_id(), chat_id);
|
||||
@@ -1663,7 +1668,7 @@ async fn test_add_info_msg_with_cmd() -> Result<()> {
|
||||
chat_id,
|
||||
"foo bar info",
|
||||
SystemMessage::EphemeralTimerChanged,
|
||||
10000,
|
||||
time(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
@@ -1926,19 +1931,31 @@ 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_chat(&t, ProtectionStatus::Unprotected, "a chat").await?;
|
||||
let chat_id = create_group_ex(&t, None, "a chat").await?;
|
||||
let color1 = Chat::load_from_db(&t, chat_id).await?.get_color(&t).await?;
|
||||
assert_eq!(color1, 0x008772);
|
||||
assert_eq!(color1, 0x6239dc);
|
||||
|
||||
// upper-/lowercase makes a difference for the colors, these are different groups
|
||||
// (in contrast to email addresses, where upper-/lowercase is ignored in practise)
|
||||
let t = TestContext::new().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "A CHAT").await?;
|
||||
let chat_id = create_group_ex(&t, None, "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],
|
||||
@@ -2101,7 +2118,7 @@ async fn test_forward_basic() -> Result<()> {
|
||||
forward_msgs(&bob, &[msg.id], bob_chat.get_id()).await?;
|
||||
|
||||
let forwarded_msg = bob.pop_sent_msg().await;
|
||||
assert_eq!(bob_chat.id.get_msg_cnt(&bob).await?, 2);
|
||||
assert_eq!(bob_chat.id.get_msg_cnt(&bob).await?, E2EE_INFO_MSGS + 2);
|
||||
assert_ne!(
|
||||
forwarded_msg.load_from_db().await.rfc724_mid,
|
||||
msg.rfc724_mid,
|
||||
@@ -2129,7 +2146,7 @@ async fn test_forward_info_msg() -> Result<()> {
|
||||
assert!(msg1.get_text().contains("bob@example.net"));
|
||||
|
||||
let chat_id2 = ChatId::create_for_contact(alice, bob_id).await?;
|
||||
assert_eq!(get_chat_msgs(alice, chat_id2).await?.len(), 0);
|
||||
assert_eq!(get_chat_msgs(alice, chat_id2).await?.len(), E2EE_INFO_MSGS);
|
||||
forward_msgs(alice, &[msg1.id], chat_id2).await?;
|
||||
let msg2 = alice.get_last_msg_in(chat_id2).await;
|
||||
assert!(!msg2.is_info()); // forwarded info-messages lose their info-state
|
||||
@@ -2277,14 +2294,19 @@ async fn test_only_minimal_data_are_forwarded() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_save_msgs() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.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?;
|
||||
@@ -2302,6 +2324,8 @@ 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!(
|
||||
@@ -2515,22 +2539,34 @@ async fn test_resend_own_message() -> Result<()> {
|
||||
let sent1_ts_sent = msg.timestamp_sent;
|
||||
assert_eq!(msg.get_text(), "alice->bob");
|
||||
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 2);
|
||||
assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 1);
|
||||
assert_eq!(
|
||||
get_chat_msgs(&bob, msg.chat_id).await?.len(),
|
||||
E2EE_INFO_MSGS + 1
|
||||
);
|
||||
bob.recv_msg(&sent2).await;
|
||||
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3);
|
||||
assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 2);
|
||||
assert_eq!(
|
||||
get_chat_msgs(&bob, msg.chat_id).await?.len(),
|
||||
E2EE_INFO_MSGS + 2
|
||||
);
|
||||
let received = bob.recv_msg_opt(&sent3).await;
|
||||
// No message should actually be added since we already know this message:
|
||||
assert!(received.is_none());
|
||||
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3);
|
||||
assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 2);
|
||||
assert_eq!(
|
||||
get_chat_msgs(&bob, msg.chat_id).await?.len(),
|
||||
E2EE_INFO_MSGS + 2
|
||||
);
|
||||
|
||||
// Fiona does not receive the first message, however, due to resending, she has a similar view as Alice and Bob
|
||||
fiona.recv_msg(&sent2).await;
|
||||
let msg = fiona.recv_msg(&sent3).await;
|
||||
assert_eq!(msg.get_text(), "alice->bob");
|
||||
assert_eq!(get_chat_contacts(&fiona, msg.chat_id).await?.len(), 3);
|
||||
assert_eq!(get_chat_msgs(&fiona, msg.chat_id).await?.len(), 2);
|
||||
assert_eq!(
|
||||
get_chat_msgs(&fiona, msg.chat_id).await?.len(),
|
||||
E2EE_INFO_MSGS + 2
|
||||
);
|
||||
let msg_from = Contact::get_by_id(&fiona, msg.get_from_id()).await?;
|
||||
assert_eq!(msg_from.get_addr(), "alice@example.org");
|
||||
assert!(sent1_ts_sent < msg.timestamp_sent);
|
||||
@@ -2931,6 +2967,105 @@ async fn test_broadcast_channel_protected_listid() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that if Bob leaves a broadcast channel,
|
||||
/// Alice (the channel owner) won't see him as a member anymore,
|
||||
/// but won't be notified about this in any way.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_leave_broadcast() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
tcm.section("Alice creates broadcast channel with Bob.");
|
||||
let alice_chat_id = create_broadcast(alice, "foo".to_string()).await?;
|
||||
let bob_contact = alice.add_or_lookup_contact(bob).await.id;
|
||||
add_contact_to_chat(alice, alice_chat_id, bob_contact).await?;
|
||||
|
||||
tcm.section("Alice sends first message to broadcast.");
|
||||
let sent_msg = alice.send_text(alice_chat_id, "Hello!").await;
|
||||
let bob_msg = bob.recv_msg(&sent_msg).await;
|
||||
|
||||
assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 1);
|
||||
|
||||
// Clear events so that we can later check
|
||||
// that the 'Broadcast channel left' message didn't trigger IncomingMsg:
|
||||
alice.evtracker.clear_events();
|
||||
|
||||
// Shift the time so that we can later check the "Broadcast channel left" message's timestamp:
|
||||
SystemTime::shift(Duration::from_secs(60));
|
||||
|
||||
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;
|
||||
alice.recv_msg_trash(&leave_msg).await;
|
||||
|
||||
assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 0);
|
||||
|
||||
alice.emit_event(EventType::Test);
|
||||
alice
|
||||
.evtracker
|
||||
.get_matching(|ev| match ev {
|
||||
EventType::Test => true,
|
||||
EventType::IncomingMsg { .. } => {
|
||||
panic!("'Broadcast channel left' message should be silent")
|
||||
}
|
||||
EventType::MsgsNoticed(..) => {
|
||||
panic!("'Broadcast channel left' message shouldn't clear notifications")
|
||||
}
|
||||
EventType::MsgsChanged { .. } => {
|
||||
panic!("Broadcast channels should be left silently, without any message");
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that if Bob leaves a broadcast channel with one device,
|
||||
/// the other device shows a correct info message "You left the channel.".
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_leave_broadcast_multidevice() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob0 = &tcm.bob().await;
|
||||
let bob1 = &tcm.bob().await;
|
||||
|
||||
tcm.section("Alice creates broadcast channel with Bob.");
|
||||
let alice_chat_id = create_broadcast(alice, "foo".to_string()).await?;
|
||||
let bob_contact = alice.add_or_lookup_contact(bob0).await.id;
|
||||
add_contact_to_chat(alice, alice_chat_id, bob_contact).await?;
|
||||
|
||||
tcm.section("Alice sends first message to broadcast.");
|
||||
let sent_msg = alice.send_text(alice_chat_id, "Hello!").await;
|
||||
let bob0_hello = bob0.recv_msg(&sent_msg).await;
|
||||
let bob1_hello = bob1.recv_msg(&sent_msg).await;
|
||||
|
||||
tcm.section("Bob leaves the broadcast channel with his first device.");
|
||||
let bob_chat_id = bob0_hello.chat_id;
|
||||
bob_chat_id.accept(bob0).await?;
|
||||
remove_contact_from_chat(bob0, bob_chat_id, ContactId::SELF).await?;
|
||||
|
||||
let leave_msg = bob0.pop_sent_msg().await;
|
||||
let parsed = MimeMessage::from_bytes(bob1, leave_msg.payload().as_bytes(), None).await?;
|
||||
assert_eq!(
|
||||
parsed.parts[0].msg,
|
||||
stock_str::msg_group_left_remote(bob0).await
|
||||
);
|
||||
|
||||
let rcvd = bob1.recv_msg(&leave_msg).await;
|
||||
|
||||
assert_eq!(rcvd.chat_id, bob1_hello.chat_id);
|
||||
assert!(rcvd.is_info());
|
||||
assert_eq!(rcvd.get_info_type(), SystemMessage::MemberRemovedFromGroup);
|
||||
assert_eq!(rcvd.text, stock_str::msg_you_left_broadcast(bob1).await);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_for_contact_with_blocked() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
@@ -3035,6 +3170,30 @@ async fn test_chat_get_encryption_info() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_out_failed_on_all_keys_missing() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
|
||||
let bob_chat_id = bob
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "", &[alice, fiona])
|
||||
.await;
|
||||
bob.send_text(bob_chat_id, "Gossiping Fiona's key").await;
|
||||
alice
|
||||
.recv_msg(&bob.send_text(bob_chat_id, "No key gossip").await)
|
||||
.await;
|
||||
SystemTime::shift(Duration::from_secs(60));
|
||||
remove_contact_from_chat(bob, bob_chat_id, ContactId::SELF).await?;
|
||||
let alice_chat_id = alice.recv_msg(&bob.pop_sent_msg().await).await.chat_id;
|
||||
alice_chat_id.accept(alice).await?;
|
||||
let mut msg = Message::new_text("Hi".to_string());
|
||||
send_msg(alice, alice_chat_id, &mut msg).await.ok();
|
||||
assert_eq!(msg.id.get_state(alice).await?, MessageState::OutFailed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_chat_media() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
@@ -3998,7 +4157,7 @@ async fn test_past_members() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn non_member_cannot_modify_member_list() -> Result<()> {
|
||||
async fn test_non_member_cannot_modify_member_list() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
let alice = &tcm.alice().await;
|
||||
@@ -4030,6 +4189,12 @@ async fn non_member_cannot_modify_member_list() -> Result<()> {
|
||||
alice.recv_msg_trash(&bob_sent_add_msg).await;
|
||||
assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 1);
|
||||
|
||||
// The same for removal.
|
||||
let bob_alice_contact_id = bob.add_or_lookup_contact_id(alice).await;
|
||||
remove_contact_from_chat(bob, bob_chat_id, bob_alice_contact_id).await?;
|
||||
let bob_sent_add_msg = bob.pop_sent_msg().await;
|
||||
alice.recv_msg_trash(&bob_sent_add_msg).await;
|
||||
assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4349,13 +4514,13 @@ async fn test_receive_edit_request_after_removal() -> Result<()> {
|
||||
let bob_msg = bob.recv_msg(&sent1).await;
|
||||
let bob_chat_id = bob_msg.chat_id;
|
||||
assert_eq!(bob_msg.text, "zext me in delra.cat");
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, 1);
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS + 1);
|
||||
|
||||
delete_msgs(bob, &[bob_msg.id]).await?;
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, 0);
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS);
|
||||
|
||||
bob.recv_msg_trash(&sent2).await;
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, 0);
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -4403,17 +4568,6 @@ async fn test_cannot_send_edit_request() -> Result<()> {
|
||||
.is_err()
|
||||
);
|
||||
|
||||
// Videochat invitations cannot be edited
|
||||
alice
|
||||
.set_config(Config::WebrtcInstance, Some("https://foo.bar"))
|
||||
.await?;
|
||||
let msg_id = send_videochat_invitation(alice, chat_id).await?;
|
||||
assert!(
|
||||
send_edit_request(alice, msg_id, "bar".to_string())
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
// If not text was given initally, there is nothing to edit
|
||||
// (this also avoids complexity in UI element changes; focus is typos and rewordings)
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
@@ -4444,28 +4598,34 @@ async fn test_send_delete_request() -> Result<()> {
|
||||
// Alice sends a message, then sends a deletion request
|
||||
let sent1 = alice.send_text(alice_chat.id, "wtf").await;
|
||||
let alice_msg = sent1.load_from_db().await;
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, 2);
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, E2EE_INFO_MSGS + 2);
|
||||
|
||||
message::delete_msgs_ex(alice, &[alice_msg.id], true).await?;
|
||||
let sent2 = alice.pop_sent_msg().await;
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, 1);
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, E2EE_INFO_MSGS + 1);
|
||||
|
||||
// Bob receives both messages and has nothing the end
|
||||
let bob_msg = bob.recv_msg(&sent1).await;
|
||||
assert_eq!(bob_msg.text, "wtf");
|
||||
assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, 2);
|
||||
assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS + 2);
|
||||
|
||||
bob.recv_msg_opt(&sent2).await;
|
||||
assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, 1);
|
||||
assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS + 1);
|
||||
|
||||
// Alice has another device, and there is also nothing at the end
|
||||
let alice2 = &tcm.alice().await;
|
||||
alice2.recv_msg(&sent0).await;
|
||||
let alice2_msg = alice2.recv_msg(&sent1).await;
|
||||
assert_eq!(alice2_msg.chat_id.get_msg_cnt(alice2).await?, 2);
|
||||
assert_eq!(
|
||||
alice2_msg.chat_id.get_msg_cnt(alice2).await?,
|
||||
E2EE_INFO_MSGS + 2
|
||||
);
|
||||
|
||||
alice2.recv_msg_opt(&sent2).await;
|
||||
assert_eq!(alice2_msg.chat_id.get_msg_cnt(alice2).await?, 1);
|
||||
assert_eq!(
|
||||
alice2_msg.chat_id.get_msg_cnt(alice2).await?,
|
||||
E2EE_INFO_MSGS + 1
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -4593,6 +4753,42 @@ async fn test_no_key_contacts_in_adhoc_chats() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that key-contacts cannot be added to an unencrypted (ad hoc) group and the group and
|
||||
/// messages report that they are unencrypted.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_unencrypted_group_chat() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
|
||||
let chat_id = create_group_ex(alice, None, "Group chat").await?;
|
||||
let bob_key_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let charlie_address_contact_id = alice.add_or_lookup_address_contact_id(charlie).await;
|
||||
|
||||
let res = add_contact_to_chat(alice, chat_id, bob_key_contact_id).await;
|
||||
assert!(res.is_err());
|
||||
|
||||
add_contact_to_chat(alice, chat_id, charlie_address_contact_id).await?;
|
||||
|
||||
let chat = Chat::load_from_db(alice, chat_id).await?;
|
||||
assert!(!chat.is_encrypted(alice).await?);
|
||||
let sent_msg = alice.send_text(chat_id, "Hello").await;
|
||||
let msg = Message::load_from_db(alice, sent_msg.sender_msg_id).await?;
|
||||
assert!(!msg.get_showpadlock());
|
||||
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<()> {
|
||||
@@ -4623,3 +4819,25 @@ 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(())
|
||||
}
|
||||
|
||||
@@ -245,9 +245,6 @@ 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
|
||||
@@ -491,6 +488,8 @@ mod tests {
|
||||
use crate::stock_str::StockMessage;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::TestContextManager;
|
||||
use crate::tools::SystemTime;
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_try_load() {
|
||||
@@ -513,6 +512,8 @@ mod tests {
|
||||
assert_eq!(chats.get_chat_id(1).unwrap(), chat_id2);
|
||||
assert_eq!(chats.get_chat_id(2).unwrap(), chat_id1);
|
||||
|
||||
SystemTime::shift(Duration::from_secs(5));
|
||||
|
||||
// New drafts are sorted to the top
|
||||
// We have to set a draft on the other two messages, too, as
|
||||
// chat timestamps are only exact to the second and sorting by timestamp
|
||||
|
||||
46
src/color.rs
46
src/color.rs
@@ -1,38 +1,39 @@
|
||||
//! Implementation of Consistent Color Generation.
|
||||
//! Color generation.
|
||||
//!
|
||||
//! 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;
|
||||
//! 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};
|
||||
use sha1::{Digest, Sha1};
|
||||
|
||||
/// Converts an identifier to Hue angle.
|
||||
fn str_to_angle(s: &str) -> f64 {
|
||||
fn str_to_angle(s: &str) -> f32 {
|
||||
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));
|
||||
f64::from(checksum) / 65536.0 * 360.0
|
||||
f32::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((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
|
||||
fn rgb_to_u32(rgb: Rgb<u8>) -> u32 {
|
||||
65536 * u32::from(rgb.r) + 256 * u32::from(rgb.g) + u32::from(rgb.b)
|
||||
}
|
||||
|
||||
/// Converts an identifier to RGB color.
|
||||
///
|
||||
/// 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.
|
||||
/// Lightness is set to half (0.5) to make colors suitable both for light and dark theme.
|
||||
pub fn str_to_color(s: &str) -> u32 {
|
||||
rgb_to_u32(hsluv_to_rgb((str_to_angle(s), 100.0, 50.0)))
|
||||
let lightness = 0.5;
|
||||
let chroma = 0.23;
|
||||
let angle = str_to_angle(s);
|
||||
let oklch = Oklch::new(lightness, chroma, angle);
|
||||
let rgb = oklch.to_rgb(TransferFunction::Srgb);
|
||||
|
||||
rgb_to_u32(rgb)
|
||||
}
|
||||
|
||||
/// Returns color as a "#RRGGBB" `String` where R, G, B are hex digits.
|
||||
@@ -45,6 +46,7 @@ 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>
|
||||
@@ -57,11 +59,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_rgb_to_u32() {
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,10 +151,6 @@ pub enum Config {
|
||||
/// setting up a second device, or receiving a sync message.
|
||||
BccSelf,
|
||||
|
||||
/// True if encryption is preferred according to Autocrypt standard.
|
||||
#[strum(props(default = "1"))]
|
||||
E2eeEnabled,
|
||||
|
||||
/// True if Message Delivery Notifications (read receipts) should
|
||||
/// be sent and requested.
|
||||
#[strum(props(default = "1"))]
|
||||
@@ -350,9 +346,6 @@ pub enum Config {
|
||||
/// Unset, when quota falls below minimal warning threshold again.
|
||||
QuotaExceeding,
|
||||
|
||||
/// address to webrtc instance to use for videochats
|
||||
WebrtcInstance,
|
||||
|
||||
/// Timestamp of the last time housekeeping was run
|
||||
LastHousekeeping,
|
||||
|
||||
@@ -369,6 +362,9 @@ pub enum Config {
|
||||
#[strum(props(default = "0"))]
|
||||
DisableIdle,
|
||||
|
||||
/// Timestamp of the next check for donation request need.
|
||||
DonationRequestNextCheck,
|
||||
|
||||
/// Defines the max. size (in bytes) of messages downloaded automatically.
|
||||
/// 0 = no limit.
|
||||
#[strum(props(default = "0"))]
|
||||
@@ -414,16 +410,6 @@ pub enum Config {
|
||||
#[strum(props(default = "172800"))]
|
||||
GossipPeriod,
|
||||
|
||||
/// Feature flag for verified 1:1 chats; the UI should set it
|
||||
/// to 1 if it supports verified 1:1 chats.
|
||||
/// Regardless of this setting, `chat.is_protected()` returns true while the key is verified,
|
||||
/// and when the key changes, an info message is posted into the chat.
|
||||
/// 0=Nothing else happens when the key changes.
|
||||
/// 1=After the key changed, `can_send()` returns false and `is_protection_broken()` returns true
|
||||
/// until `chat_id.accept()` is called.
|
||||
#[strum(props(default = "0"))]
|
||||
VerifiedOneOnOneChats,
|
||||
|
||||
/// Row ID of the key in the `keypairs` table
|
||||
/// used for signatures, encryption to self and included in `Autocrypt` header.
|
||||
KeyId,
|
||||
@@ -451,6 +437,9 @@ pub enum Config {
|
||||
/// to avoid encrypting it differently and
|
||||
/// storing the same token multiple times on the server.
|
||||
EncryptedDeviceToken,
|
||||
|
||||
/// Return an error from `receive_imf_inner()` for a fully downloaded message. For tests.
|
||||
FailOnReceivingFullMsg,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -702,7 +691,6 @@ impl Context {
|
||||
Config::Socks5Enabled
|
||||
| Config::ProxyEnabled
|
||||
| Config::BccSelf
|
||||
| Config::E2eeEnabled
|
||||
| Config::MdnsEnabled
|
||||
| Config::SentboxWatch
|
||||
| Config::MvboxMove
|
||||
@@ -731,7 +719,7 @@ impl Context {
|
||||
Self::check_config(key, value)?;
|
||||
|
||||
let _pause = match key.needs_io_restart() {
|
||||
true => self.scheduler.pause(self.clone()).await?,
|
||||
true => self.scheduler.pause(self).await?,
|
||||
_ => Default::default(),
|
||||
};
|
||||
self.set_config_internal(key, value).await?;
|
||||
|
||||
@@ -137,7 +137,7 @@ impl Context {
|
||||
|
||||
let res = self
|
||||
.inner_configure(param)
|
||||
.race(cancel_channel.recv().map(|_| Err(format_err!("Cancelled"))))
|
||||
.race(cancel_channel.recv().map(|_| Err(format_err!("Canceled"))))
|
||||
.await;
|
||||
|
||||
self.free_ongoing().await;
|
||||
|
||||
@@ -106,7 +106,7 @@ fn parse_server<B: BufRead>(
|
||||
}
|
||||
}
|
||||
Event::Text(ref event) => {
|
||||
let val = event.unescape().unwrap_or_default().trim().to_owned();
|
||||
let val = event.xml_content().unwrap_or_default().trim().to_owned();
|
||||
|
||||
match tag_config {
|
||||
MozConfigTag::Hostname => hostname = Some(val),
|
||||
|
||||
@@ -79,7 +79,7 @@ fn parse_protocol<B: BufRead>(
|
||||
}
|
||||
}
|
||||
Event::Text(ref e) => {
|
||||
let val = e.unescape().unwrap_or_default();
|
||||
let val = e.xml_content().unwrap_or_default();
|
||||
|
||||
if let Some(ref tag) = current_tag {
|
||||
match tag.as_str() {
|
||||
@@ -123,7 +123,7 @@ fn parse_redirecturl<B: BufRead>(
|
||||
let mut buf = Vec::new();
|
||||
match reader.read_event_into(&mut buf)? {
|
||||
Event::Text(ref e) => {
|
||||
let val = e.unescape().unwrap_or_default();
|
||||
let val = e.xml_content().unwrap_or_default();
|
||||
Ok(val.trim().to_string())
|
||||
}
|
||||
_ => Ok("".to_string()),
|
||||
|
||||
@@ -60,23 +60,6 @@ pub enum MediaQuality {
|
||||
Worse = 1,
|
||||
}
|
||||
|
||||
/// Video chat URL type.
|
||||
#[derive(
|
||||
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||
)]
|
||||
#[repr(i8)]
|
||||
pub enum VideochatType {
|
||||
/// Unknown type.
|
||||
#[default]
|
||||
Unknown = 0,
|
||||
|
||||
/// [basicWebRTC](https://github.com/cracker0dks/basicwebrtc) instance.
|
||||
BasicWebrtc = 1,
|
||||
|
||||
/// [Jitsi Meet](https://jitsi.org/jitsi-meet/) instance.
|
||||
Jitsi = 2,
|
||||
}
|
||||
|
||||
pub const DC_HANDSHAKE_CONTINUE_NORMAL_PROCESSING: i32 = 0x01;
|
||||
pub const DC_HANDSHAKE_STOP_NORMAL_PROCESSING: i32 = 0x02;
|
||||
pub const DC_HANDSHAKE_ADD_DELETE_JOB: i32 = 0x04;
|
||||
@@ -95,10 +78,11 @@ pub const DC_GCL_ADDRESS: u32 = 0x04;
|
||||
pub(crate) const DC_RESEND_USER_AVATAR_DAYS: i64 = 14;
|
||||
|
||||
// warn about an outdated app after a given number of days.
|
||||
// as we use the "provider-db generation date" as reference (that might not be updated very often)
|
||||
// and as not all system get speedy updates,
|
||||
// reference is the release date.
|
||||
// as not all system get speedy updates,
|
||||
// do not use too small value that will annoy users checking for nonexistent updates.
|
||||
pub(crate) const DC_OUTDATED_WARNING_DAYS: i64 = 365;
|
||||
// "90 days" has proven to be too short at some point (user were informed but there was no update)
|
||||
pub(crate) const DC_OUTDATED_WARNING_DAYS: i64 = 183;
|
||||
|
||||
/// messages that should be deleted get this chat_id; the messages are deleted from the working thread later then. This is also needed as rfc724_mid should be preset as long as the message is not deleted on the server (otherwise it is downloaded again)
|
||||
pub const DC_CHAT_ID_TRASH: ChatId = ChatId::new(3);
|
||||
@@ -307,16 +291,4 @@ mod tests {
|
||||
assert_eq!(MediaQuality::Balanced, MediaQuality::from_i32(0).unwrap());
|
||||
assert_eq!(MediaQuality::Worse, MediaQuality::from_i32(1).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_videochattype_values() {
|
||||
// values may be written to disk and must not change
|
||||
assert_eq!(VideochatType::Unknown, VideochatType::default());
|
||||
assert_eq!(VideochatType::Unknown, VideochatType::from_i32(0).unwrap());
|
||||
assert_eq!(
|
||||
VideochatType::BasicWebrtc,
|
||||
VideochatType::from_i32(1).unwrap()
|
||||
);
|
||||
assert_eq!(VideochatType::Jitsi, VideochatType::from_i32(2).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
120
src/contact.rs
120
src/contact.rs
@@ -21,7 +21,7 @@ use tokio::task;
|
||||
use tokio::time::{Duration, timeout};
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{ChatId, ChatIdBlocked, ProtectionStatus};
|
||||
use crate::chat::ChatId;
|
||||
use crate::color::str_to_color;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{self, Blocked, Chattype};
|
||||
@@ -37,7 +37,7 @@ use crate::mimeparser::AvatarAction;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::sync::{self, Sync::*};
|
||||
use crate::tools::{SystemTime, duration_to_str, get_abs_path, time};
|
||||
use crate::{chat, chatlist_events, stock_str};
|
||||
use crate::{chat, chatlist_events, ensure_and_debug_assert_ne, stock_str};
|
||||
|
||||
/// Time during which a contact is considered as seen recently.
|
||||
const SEEN_RECENTLY_SECONDS: i64 = 600;
|
||||
@@ -755,7 +755,19 @@ impl Contact {
|
||||
self.is_bot
|
||||
}
|
||||
|
||||
/// Check if an e-mail address belongs to a known and unblocked 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.
|
||||
///
|
||||
/// Known and unblocked contacts will be returned by `get_contacts()`.
|
||||
///
|
||||
@@ -795,14 +807,28 @@ impl Contact {
|
||||
.query_get_value(
|
||||
"SELECT id FROM contacts
|
||||
WHERE addr=?1 COLLATE NOCASE
|
||||
AND fingerprint='' -- Do not lookup key-contacts
|
||||
AND id>?2 AND origin>=?3 AND (? OR blocked=?)",
|
||||
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",
|
||||
(
|
||||
&addr_normalized,
|
||||
ContactId::LAST_SPECIAL,
|
||||
min_origin as u32,
|
||||
blocked.is_none(),
|
||||
blocked.unwrap_or_default(),
|
||||
blocked.unwrap_or(Blocked::Not),
|
||||
Chattype::Single,
|
||||
constants::DC_CHAT_ID_LAST_SPECIAL,
|
||||
blocked.unwrap_or(Blocked::Not),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
@@ -1538,7 +1564,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_address_contact_icon(context).await?));
|
||||
return Ok(Some(chat::get_unencrypted_icon(context).await?));
|
||||
}
|
||||
if let Some(image_rel) = self.param.get(Param::ProfileImage) {
|
||||
if !image_rel.is_empty() {
|
||||
@@ -1549,11 +1575,18 @@ impl Contact {
|
||||
}
|
||||
|
||||
/// Get a color for the contact.
|
||||
/// The color is calculated from the contact's email address
|
||||
/// and can be used for an fallback avatar with white initials
|
||||
/// The color is calculated from the contact's fingerprint (for key-contacts)
|
||||
/// or email address (for address-contacts) and can be used
|
||||
/// for an fallback avatar with white initials
|
||||
/// as well as for headlines in bubbles of group chats.
|
||||
pub fn get_color(&self) -> u32 {
|
||||
str_to_color(&self.addr.to_lowercase())
|
||||
if let Some(fingerprint) = self.fingerprint() {
|
||||
str_to_color(&fingerprint.hex())
|
||||
} else if self.id == ContactId::SELF {
|
||||
0x808080
|
||||
} else {
|
||||
str_to_color(&self.addr.to_lowercase())
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the contact's status.
|
||||
@@ -1619,29 +1652,6 @@ impl Contact {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns if the contact profile title should display a green checkmark.
|
||||
///
|
||||
/// This generally should be consistent with the 1:1 chat with the contact
|
||||
/// so 1:1 chat with the contact and the contact profile
|
||||
/// either both display the green checkmark or both don't display a green checkmark.
|
||||
///
|
||||
/// UI often knows beforehand if a chat exists and can also call
|
||||
/// `chat.is_protected()` (if there is a chat)
|
||||
/// or `contact.is_verified()` (if there is no chat) directly.
|
||||
/// This is often easier and also skips some database calls.
|
||||
pub async fn is_profile_verified(&self, context: &Context) -> Result<bool> {
|
||||
let contact_id = self.id;
|
||||
|
||||
if let Some(ChatIdBlocked { id: chat_id, .. }) =
|
||||
ChatIdBlocked::lookup_by_contact(context, contact_id).await?
|
||||
{
|
||||
Ok(chat_id.is_protected(context).await? == ProtectionStatus::Protected)
|
||||
} else {
|
||||
// 1:1 chat does not exist.
|
||||
Ok(self.is_verified(context).await?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of real (i.e. non-special) contacts in the database.
|
||||
pub async fn get_real_cnt(context: &Context) -> Result<usize> {
|
||||
if !context.sql.is_open().await {
|
||||
@@ -1734,8 +1744,7 @@ pub(crate) async fn set_blocked(
|
||||
) -> Result<()> {
|
||||
ensure!(
|
||||
!contact_id.is_special(),
|
||||
"Can't block special contact {}",
|
||||
contact_id
|
||||
"Can't block special contact {contact_id}"
|
||||
);
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
|
||||
@@ -1917,15 +1926,21 @@ pub(crate) async fn update_last_seen(
|
||||
}
|
||||
|
||||
/// Marks contact `contact_id` as verified by `verifier_id`.
|
||||
///
|
||||
/// `verifier_id == None` means that the verifier is unknown.
|
||||
pub(crate) async fn mark_contact_id_as_verified(
|
||||
context: &Context,
|
||||
contact_id: ContactId,
|
||||
verifier_id: ContactId,
|
||||
verifier_id: Option<ContactId>,
|
||||
) -> Result<()> {
|
||||
debug_assert_ne!(
|
||||
contact_id, verifier_id,
|
||||
"Contact cannot be verified by self"
|
||||
ensure_and_debug_assert_ne!(contact_id, ContactId::SELF,);
|
||||
ensure_and_debug_assert_ne!(
|
||||
Some(contact_id),
|
||||
verifier_id,
|
||||
"Contact cannot be verified by self",
|
||||
);
|
||||
let by_self = verifier_id == Some(ContactId::SELF);
|
||||
let mut verifier_id = verifier_id.unwrap_or(contact_id);
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
@@ -1938,20 +1953,33 @@ pub(crate) async fn mark_contact_id_as_verified(
|
||||
bail!("Non-key-contact {contact_id} cannot be verified");
|
||||
}
|
||||
if verifier_id != ContactId::SELF {
|
||||
let verifier_fingerprint: String = transaction.query_row(
|
||||
"SELECT fingerprint FROM contacts WHERE id=?",
|
||||
(verifier_id,),
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
let (verifier_fingerprint, verifier_verifier_id): (String, ContactId) = transaction
|
||||
.query_row(
|
||||
"SELECT fingerprint, verifier FROM contacts WHERE id=?",
|
||||
(verifier_id,),
|
||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||
)?;
|
||||
if verifier_fingerprint.is_empty() {
|
||||
bail!(
|
||||
"Contact {contact_id} cannot be verified by non-key-contact {verifier_id}"
|
||||
);
|
||||
}
|
||||
ensure!(
|
||||
verifier_id == contact_id || verifier_verifier_id != ContactId::UNDEFINED,
|
||||
"Contact {contact_id} cannot be verified by unverified contact {verifier_id}",
|
||||
);
|
||||
if verifier_verifier_id == verifier_id {
|
||||
// Avoid introducing incorrect reverse chains: if the verifier itself has an
|
||||
// unknown verifier, it may be `contact_id` actually (directly or indirectly) on
|
||||
// the other device (which is needed for getting "verified by unknown contact"
|
||||
// in the first place).
|
||||
verifier_id = contact_id;
|
||||
}
|
||||
}
|
||||
transaction.execute(
|
||||
"UPDATE contacts SET verifier=? WHERE id=?",
|
||||
(verifier_id, contact_id),
|
||||
"UPDATE contacts SET verifier=?1
|
||||
WHERE id=?2 AND (verifier=0 OR verifier=id OR ?3)",
|
||||
(verifier_id, contact_id, by_self),
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use deltachat_contact_tools::{addr_cmp, may_be_valid_addr};
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{Chat, get_chat_contacts, send_text_msg};
|
||||
use crate::chat::{Chat, ProtectionStatus, get_chat_contacts, send_text_msg};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::test_utils::{self, TestContext, TestContextManager, TimeShiftFalsePositiveNote};
|
||||
|
||||
#[test]
|
||||
@@ -759,7 +760,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, 0xA739FF);
|
||||
assert_eq!(color1, 0x4844e2);
|
||||
|
||||
let t = TestContext::new().await;
|
||||
let contact_id = Contact::create(&t, "prename name", "name@example.net").await?;
|
||||
@@ -773,6 +774,20 @@ async fn test_contact_get_color() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_self_color_vs_key() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.unconfigured().await;
|
||||
t.configure_addr("alice@example.org").await;
|
||||
assert!(t.is_configured().await?);
|
||||
let color = Contact::get_by_id(t, ContactId::SELF).await?.get_color();
|
||||
assert_eq!(color, 0x808080);
|
||||
get_securejoin_qr(t, None).await?;
|
||||
let color1 = Contact::get_by_id(t, ContactId::SELF).await?.get_color();
|
||||
assert_ne!(color1, color);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_contact_get_encrinfo() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -1035,6 +1050,50 @@ 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();
|
||||
@@ -1072,6 +1131,7 @@ 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;
|
||||
@@ -1081,6 +1141,7 @@ 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(())
|
||||
}
|
||||
@@ -1256,7 +1317,6 @@ async fn test_self_is_verified() -> Result<()> {
|
||||
|
||||
let contact = Contact::get_by_id(&alice, ContactId::SELF).await?;
|
||||
assert_eq!(contact.is_verified(&alice).await?, true);
|
||||
assert!(contact.is_profile_verified(&alice).await?);
|
||||
assert!(contact.get_verifier_id(&alice).await?.is_none());
|
||||
assert!(contact.is_key_contact());
|
||||
|
||||
|
||||
@@ -27,13 +27,14 @@ use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::imap::{FolderMeaning, Imap, ServerMetadata};
|
||||
use crate::key::{load_self_secret_key, self_fingerprint};
|
||||
use crate::log::{info, warn};
|
||||
use crate::logged_debug_assert;
|
||||
use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam};
|
||||
use crate::message::{self, Message, MessageState, MsgId};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peer_channels::Iroh;
|
||||
use crate::push::PushSubscriber;
|
||||
use crate::quota::QuotaInfo;
|
||||
use crate::scheduler::{SchedulerState, convert_folder_meaning};
|
||||
use crate::scheduler::{ConnectivityStore, SchedulerState, convert_folder_meaning};
|
||||
use crate::sql::Sql;
|
||||
use crate::stock_str::StockStrings;
|
||||
use crate::timesmearing::SmearedTimestamp;
|
||||
@@ -303,6 +304,10 @@ 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.
|
||||
@@ -332,6 +337,15 @@ impl Default for RunningState {
|
||||
/// about the context on top of the information here.
|
||||
pub fn get_info() -> BTreeMap<&'static str, String> {
|
||||
let mut res = BTreeMap::new();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
res.insert(
|
||||
"debug_assertions",
|
||||
"On - DO NOT RELEASE THIS BUILD".to_string(),
|
||||
);
|
||||
#[cfg(not(debug_assertions))]
|
||||
res.insert("debug_assertions", "Off".to_string());
|
||||
|
||||
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
|
||||
res.insert("sqlite_version", rusqlite::version().to_string());
|
||||
res.insert("arch", (std::mem::size_of::<usize>() * 8).to_string());
|
||||
@@ -463,6 +477,7 @@ 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 {
|
||||
@@ -492,7 +507,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.clone()).await;
|
||||
self.scheduler.start(self).await;
|
||||
}
|
||||
|
||||
/// Stops the IO scheduler.
|
||||
@@ -569,7 +584,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.clone()).await?;
|
||||
let _pause_guard = self.scheduler.pause(self).await?;
|
||||
|
||||
// Start a new dedicated connection.
|
||||
let mut connection = Imap::new_configured(self, channel::bounded(1).1).await?;
|
||||
@@ -660,8 +675,16 @@ impl Context {
|
||||
/// or [`Self::emit_msgs_changed_without_msg_id`] should be used
|
||||
/// instead of this function.
|
||||
pub fn emit_msgs_changed(&self, chat_id: ChatId, msg_id: MsgId) {
|
||||
debug_assert!(!chat_id.is_unset());
|
||||
debug_assert!(!msg_id.is_unset());
|
||||
logged_debug_assert!(
|
||||
self,
|
||||
!chat_id.is_unset(),
|
||||
"emit_msgs_changed: chat_id is unset."
|
||||
);
|
||||
logged_debug_assert!(
|
||||
self,
|
||||
!msg_id.is_unset(),
|
||||
"emit_msgs_changed: msg_id is unset."
|
||||
);
|
||||
|
||||
self.emit_event(EventType::MsgsChanged { chat_id, msg_id });
|
||||
chatlist_events::emit_chatlist_changed(self);
|
||||
@@ -670,7 +693,11 @@ impl Context {
|
||||
|
||||
/// Emits a MsgsChanged event with specified chat and without message id.
|
||||
pub fn emit_msgs_changed_without_msg_id(&self, chat_id: ChatId) {
|
||||
debug_assert!(!chat_id.is_unset());
|
||||
logged_debug_assert!(
|
||||
self,
|
||||
!chat_id.is_unset(),
|
||||
"emit_msgs_changed_without_msg_id: chat_id is unset."
|
||||
);
|
||||
|
||||
self.emit_event(EventType::MsgsChanged {
|
||||
chat_id,
|
||||
@@ -806,7 +833,6 @@ impl Context {
|
||||
.query_get_value("PRAGMA journal_mode;", ())
|
||||
.await?
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await?;
|
||||
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await?;
|
||||
let bcc_self = self.get_config_int(Config::BccSelf).await?;
|
||||
let sync_msgs = self.get_config_int(Config::SyncMsgs).await?;
|
||||
@@ -940,19 +966,12 @@ impl Context {
|
||||
res.insert("configured_mvbox_folder", configured_mvbox_folder);
|
||||
res.insert("configured_trash_folder", configured_trash_folder);
|
||||
res.insert("mdns_enabled", mdns_enabled.to_string());
|
||||
res.insert("e2ee_enabled", e2ee_enabled.to_string());
|
||||
res.insert("bcc_self", bcc_self.to_string());
|
||||
res.insert("sync_msgs", sync_msgs.to_string());
|
||||
res.insert("disable_idle", disable_idle.to_string());
|
||||
res.insert("private_key_count", prv_key_cnt.to_string());
|
||||
res.insert("public_key_count", pub_key_cnt.to_string());
|
||||
res.insert("fingerprint", fingerprint_str);
|
||||
res.insert(
|
||||
"webrtc_instance",
|
||||
self.get_config(Config::WebrtcInstance)
|
||||
.await?
|
||||
.unwrap_or_else(|| "<unset>".to_string()),
|
||||
);
|
||||
res.insert(
|
||||
"media_quality",
|
||||
self.get_config_int(Config::MediaQuality).await?.to_string(),
|
||||
@@ -1030,14 +1049,14 @@ impl Context {
|
||||
self.get_config_int(Config::GossipPeriod).await?.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"verified_one_on_one_chats",
|
||||
self.get_config_bool(Config::VerifiedOneOnOneChats)
|
||||
"webxdc_realtime_enabled",
|
||||
self.get_config_bool(Config::WebxdcRealtimeEnabled)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"webxdc_realtime_enabled",
|
||||
self.get_config_bool(Config::WebxdcRealtimeEnabled)
|
||||
"donation_request_next_check",
|
||||
self.get_config_i64(Config::DonationRequestNextCheck)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
@@ -1048,6 +1067,13 @@ impl Context {
|
||||
.await?
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
res.insert(
|
||||
"fail_on_receiving_full_msg",
|
||||
self.sql
|
||||
.get_raw_config("fail_on_receiving_full_msg")
|
||||
.await?
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
|
||||
let elapsed = time_elapsed(&self.creation_time);
|
||||
res.insert("uptime", duration_to_str(elapsed));
|
||||
@@ -1059,7 +1085,6 @@ impl Context {
|
||||
#[derive(Default)]
|
||||
struct ChatNumbers {
|
||||
protected: u32,
|
||||
protection_broken: u32,
|
||||
opportunistic_dc: u32,
|
||||
opportunistic_mua: u32,
|
||||
unencrypted_dc: u32,
|
||||
@@ -1095,7 +1120,6 @@ 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
|
||||
@@ -1138,8 +1162,6 @@ 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;
|
||||
@@ -1157,7 +1179,6 @@ 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);
|
||||
@@ -1187,7 +1208,7 @@ impl Context {
|
||||
.await?
|
||||
.first()
|
||||
.context("Self reporting bot vCard does not contain a contact")?;
|
||||
mark_contact_id_as_verified(self, contact_id, ContactId::SELF).await?;
|
||||
mark_contact_id_as_verified(self, contact_id, Some(ContactId::SELF)).await?;
|
||||
|
||||
let chat_id = ChatId::create_for_contact(self, contact_id).await?;
|
||||
chat_id
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{TestContext, get_chat_msg};
|
||||
use crate::test_utils::{E2EE_INFO_MSGS, TestContext, get_chat_msg};
|
||||
use crate::tools::{SystemTime, create_outgoing_rfc724_mid};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -571,7 +571,7 @@ async fn test_get_next_msgs() -> Result<()> {
|
||||
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
|
||||
assert!(alice.get_next_msgs().await?.is_empty());
|
||||
assert_eq!(alice.get_next_msgs().await?.len(), E2EE_INFO_MSGS);
|
||||
assert!(bob.get_next_msgs().await?.is_empty());
|
||||
|
||||
let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;
|
||||
|
||||
@@ -178,7 +178,8 @@ mod tests {
|
||||
let bob = TestContext::new_bob().await;
|
||||
receive_imf(&bob, attachment_mime, false).await?;
|
||||
let msg = bob.get_last_msg().await;
|
||||
assert_eq!(msg.text, "Hello from Thunderbird!");
|
||||
// Subject should be prepended because the attachment doesn't have "Chat-Version".
|
||||
assert_eq!(msg.text, "Hello, Bob! – Hello from Thunderbird!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use std::sync::LazyLock;
|
||||
|
||||
use quick_xml::{
|
||||
Reader,
|
||||
errors::Error as QuickXmlError,
|
||||
events::{BytesEnd, BytesStart, BytesText},
|
||||
};
|
||||
|
||||
@@ -132,6 +133,7 @@ fn dehtml_quick_xml(buf: &str) -> (String, String) {
|
||||
reader.config_mut().check_end_names = false;
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let mut char_buf = String::with_capacity(4);
|
||||
|
||||
loop {
|
||||
match reader.read_event_into(&mut buf) {
|
||||
@@ -140,16 +142,9 @@ fn dehtml_quick_xml(buf: &str) -> (String, String) {
|
||||
}
|
||||
Ok(quick_xml::events::Event::End(ref e)) => dehtml_endtag_cb(e, &mut dehtml),
|
||||
Ok(quick_xml::events::Event::Text(ref e)) => dehtml_text_cb(e, &mut dehtml),
|
||||
Ok(quick_xml::events::Event::CData(e)) => match e.escape() {
|
||||
Ok(e) => dehtml_text_cb(&e, &mut dehtml),
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"CDATA escape error at position {}: {:?}",
|
||||
reader.buffer_position(),
|
||||
e,
|
||||
);
|
||||
}
|
||||
},
|
||||
Ok(quick_xml::events::Event::CData(e)) => {
|
||||
str_cb(&String::from_utf8_lossy(&e as &[_]), &mut dehtml)
|
||||
}
|
||||
Ok(quick_xml::events::Event::Empty(ref e)) => {
|
||||
// Handle empty tags as a start tag immediately followed by end tag.
|
||||
// For example, `<p/>` is treated as `<p></p>`.
|
||||
@@ -159,6 +154,33 @@ fn dehtml_quick_xml(buf: &str) -> (String, String) {
|
||||
&mut dehtml,
|
||||
);
|
||||
}
|
||||
Ok(quick_xml::events::Event::GeneralRef(ref e)) => {
|
||||
match e.resolve_char_ref() {
|
||||
Err(err) => eprintln!(
|
||||
"resolve_char_ref() error at position {}: {:?}",
|
||||
reader.buffer_position(),
|
||||
err,
|
||||
),
|
||||
Ok(Some(ch)) => {
|
||||
char_buf.clear();
|
||||
char_buf.push(ch);
|
||||
str_cb(&char_buf, &mut dehtml);
|
||||
}
|
||||
Ok(None) => {
|
||||
let event_str = String::from_utf8_lossy(e);
|
||||
if let Some(s) = quick_xml::escape::resolve_html5_entity(&event_str) {
|
||||
str_cb(s, &mut dehtml);
|
||||
} else {
|
||||
// Nonstandard entity. Add escaped.
|
||||
str_cb(&format!("&{event_str};"), &mut dehtml);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(QuickXmlError::IllFormed(_)) => {
|
||||
// This is probably not HTML at all and should be left as is.
|
||||
str_cb(&String::from_utf8_lossy(&buf), &mut dehtml);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Parse html error: Error at position {}: {:?}",
|
||||
@@ -176,36 +198,36 @@ fn dehtml_quick_xml(buf: &str) -> (String, String) {
|
||||
}
|
||||
|
||||
fn dehtml_text_cb(event: &BytesText, dehtml: &mut Dehtml) {
|
||||
static LINE_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"(\r?\n)+").unwrap());
|
||||
|
||||
if dehtml.get_add_text() == AddText::YesPreserveLineEnds
|
||||
|| dehtml.get_add_text() == AddText::YesRemoveLineEnds
|
||||
{
|
||||
let event = event as &[_];
|
||||
let event_str = std::str::from_utf8(event).unwrap_or_default();
|
||||
let mut last_added = escaper::decode_html_buf_sloppy(event).unwrap_or_default();
|
||||
if event_str.starts_with(&last_added) {
|
||||
last_added = event_str.to_string();
|
||||
str_cb(event_str, dehtml);
|
||||
}
|
||||
}
|
||||
|
||||
fn str_cb(event_str: &str, dehtml: &mut Dehtml) {
|
||||
static LINE_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"(\r?\n)+").unwrap());
|
||||
|
||||
let add_text = dehtml.get_add_text();
|
||||
if add_text == AddText::YesRemoveLineEnds {
|
||||
// Replace all line ends with spaces.
|
||||
// E.g. `\r\n\r\n` is replaced with one space.
|
||||
let event_str = LINE_RE.replace_all(event_str, " ");
|
||||
|
||||
// Add a space if `event_str` starts with a space
|
||||
// and there is no whitespace at the end of the buffer yet.
|
||||
// Trim the rest of leading whitespace from `event_str`.
|
||||
let buf = dehtml.get_buf();
|
||||
if !buf.ends_with(' ') && !buf.ends_with('\n') && event_str.starts_with(' ') {
|
||||
*buf += " ";
|
||||
}
|
||||
|
||||
if dehtml.get_add_text() == AddText::YesRemoveLineEnds {
|
||||
// Replace all line ends with spaces.
|
||||
// E.g. `\r\n\r\n` is replaced with one space.
|
||||
let last_added = LINE_RE.replace_all(&last_added, " ");
|
||||
|
||||
// Add a space if `last_added` starts with a space
|
||||
// and there is no whitespace at the end of the buffer yet.
|
||||
// Trim the rest of leading whitespace from `last_added`.
|
||||
let buf = dehtml.get_buf();
|
||||
if !buf.ends_with(' ') && !buf.ends_with('\n') && last_added.starts_with(' ') {
|
||||
*buf += " ";
|
||||
}
|
||||
|
||||
*buf += last_added.trim_start();
|
||||
} else {
|
||||
*dehtml.get_buf() += LINE_RE.replace_all(&last_added, "\n").as_ref();
|
||||
}
|
||||
*buf += event_str.trim_start();
|
||||
} else if add_text == AddText::YesPreserveLineEnds {
|
||||
*dehtml.get_buf() += LINE_RE.replace_all(event_str, "\n").as_ref();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -213,17 +213,18 @@ impl Session {
|
||||
|
||||
let mut uid_message_ids: BTreeMap<u32, String> = BTreeMap::new();
|
||||
uid_message_ids.insert(uid, rfc724_mid);
|
||||
let (last_uid, _received) = self
|
||||
.fetch_many_msgs(
|
||||
context,
|
||||
folder,
|
||||
uidvalidity,
|
||||
vec![uid],
|
||||
&uid_message_ids,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
if last_uid.is_none() {
|
||||
let (sender, receiver) = async_channel::unbounded();
|
||||
self.fetch_many_msgs(
|
||||
context,
|
||||
folder,
|
||||
uidvalidity,
|
||||
vec![uid],
|
||||
&uid_message_ids,
|
||||
false,
|
||||
sender,
|
||||
)
|
||||
.await?;
|
||||
if receiver.recv().await.is_err() {
|
||||
bail!("Failed to fetch UID {uid}");
|
||||
}
|
||||
Ok(())
|
||||
@@ -237,14 +238,20 @@ impl MimeMessage {
|
||||
/// the mime-structure itself is not available.
|
||||
///
|
||||
/// The placeholder part currently contains a text with size and availability of the message;
|
||||
/// `error` is set as the part error;
|
||||
/// in the future, we may do more advanced things as previews here.
|
||||
pub(crate) async fn create_stub_from_partial_download(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
org_bytes: u32,
|
||||
error: Option<String>,
|
||||
) -> Result<()> {
|
||||
let prefix = match error {
|
||||
None => "",
|
||||
Some(_) => "[❗] ",
|
||||
};
|
||||
let mut text = format!(
|
||||
"[{}]",
|
||||
"{prefix}[{}]",
|
||||
stock_str::partial_download_msg_body(context, org_bytes).await
|
||||
);
|
||||
if let Some(delete_server_after) = context.get_config_delete_server_after().await? {
|
||||
@@ -258,9 +265,10 @@ impl MimeMessage {
|
||||
|
||||
info!(context, "Partial download: {}", text);
|
||||
|
||||
self.parts.push(Part {
|
||||
self.do_add_single_part(Part {
|
||||
typ: Viewtype::Text,
|
||||
msg: text,
|
||||
error,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
@@ -275,8 +283,9 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::chat::{get_chat_msgs, send_msg};
|
||||
use crate::ephemeral::Timer;
|
||||
use crate::message::delete_msgs;
|
||||
use crate::receive_imf::receive_imf_from_inbox;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::{E2EE_INFO_MSGS, TestContext, TestContextManager};
|
||||
|
||||
#[test]
|
||||
fn test_downloadstate_values() {
|
||||
@@ -458,7 +467,10 @@ mod tests {
|
||||
.await?;
|
||||
let msg = bob.get_last_msg().await;
|
||||
let chat_id = msg.chat_id;
|
||||
assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 1);
|
||||
assert_eq!(
|
||||
get_chat_msgs(&bob, chat_id).await?.len(),
|
||||
E2EE_INFO_MSGS + 1
|
||||
);
|
||||
assert_eq!(msg.download_state(), DownloadState::Available);
|
||||
|
||||
// downloading the status update afterwards expands to nothing and moves the placeholder to trash-chat
|
||||
@@ -471,7 +483,7 @@ mod tests {
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 0);
|
||||
assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), E2EE_INFO_MSGS);
|
||||
assert!(
|
||||
Message::load_from_db_optional(&bob, msg.id)
|
||||
.await?
|
||||
@@ -532,4 +544,43 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that fully downloading the message
|
||||
/// works even if the Message-ID already exists
|
||||
/// in the database assigned to the trash chat.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_partial_download_trashed() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
|
||||
let imf_raw = b"From: Bob <bob@example.org>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: subject\n\
|
||||
Message-ID: <first@example.org>\n\
|
||||
Date: Sun, 14 Nov 2021 00:10:00 +0000\
|
||||
Content-Type: text/plain";
|
||||
|
||||
// Download message from Bob partially.
|
||||
let partial_received_msg =
|
||||
receive_imf_from_inbox(alice, "first@example.org", imf_raw, false, Some(100000))
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_eq!(partial_received_msg.msg_ids.len(), 1);
|
||||
|
||||
// Delete the received message.
|
||||
// Not it is still in the database,
|
||||
// but in the trash chat.
|
||||
delete_msgs(alice, &[partial_received_msg.msg_ids[0]]).await?;
|
||||
|
||||
// Fully download message after deletion.
|
||||
let full_received_msg =
|
||||
receive_imf_from_inbox(alice, "first@example.org", imf_raw, false, None).await?;
|
||||
|
||||
// The message does not reappear.
|
||||
// However, `receive_imf` should not fail.
|
||||
assert!(full_received_msg.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
15
src/e2ee.rs
15
src/e2ee.rs
@@ -4,10 +4,8 @@ use std::io::Cursor;
|
||||
|
||||
use anyhow::Result;
|
||||
use mail_builder::mime::MimePart;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use crate::aheader::{Aheader, EncryptPreference};
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::key::{SignedPublicKey, load_self_public_key, load_self_secret_key};
|
||||
use crate::pgp;
|
||||
@@ -21,9 +19,7 @@ pub struct EncryptHelper {
|
||||
|
||||
impl EncryptHelper {
|
||||
pub async fn new(context: &Context) -> Result<EncryptHelper> {
|
||||
let prefer_encrypt =
|
||||
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await?)
|
||||
.unwrap_or_default();
|
||||
let prefer_encrypt = EncryptPreference::Mutual;
|
||||
let addr = context.get_primary_self_addr().await?;
|
||||
let public_key = load_self_public_key(context).await?;
|
||||
|
||||
@@ -35,9 +31,12 @@ impl EncryptHelper {
|
||||
}
|
||||
|
||||
pub fn get_aheader(&self) -> Aheader {
|
||||
let pk = self.public_key.clone();
|
||||
let addr = self.addr.to_string();
|
||||
Aheader::new(addr, pk, self.prefer_encrypt)
|
||||
Aheader {
|
||||
addr: self.addr.clone(),
|
||||
public_key: self.public_key.clone(),
|
||||
prefer_encrypt: self.prefer_encrypt,
|
||||
verified: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to encrypt the passed in `mail`.
|
||||
|
||||
@@ -277,6 +277,7 @@ 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,
|
||||
@@ -375,6 +376,10 @@ pub(crate) async fn start_chat_ephemeral_timers(context: &Context, chat_id: Chat
|
||||
/// `delete_device_after` setting or `ephemeral_timestamp` column.
|
||||
///
|
||||
/// For each message a row ID, chat id, viewtype and location ID is returned.
|
||||
///
|
||||
/// Unknown viewtypes are returned as `Viewtype::Unknown`
|
||||
/// and not as errors bubbled up, easily resulting in infinite loop or leaving messages undeleted.
|
||||
/// (Happens when viewtypes are removed or added on another device which was backup/add-second-device source)
|
||||
async fn select_expired_messages(
|
||||
context: &Context,
|
||||
now: i64,
|
||||
@@ -394,7 +399,11 @@ WHERE
|
||||
|row| {
|
||||
let id: MsgId = row.get("id")?;
|
||||
let chat_id: ChatId = row.get("chat_id")?;
|
||||
let viewtype: Viewtype = row.get("type")?;
|
||||
let viewtype: Viewtype = row
|
||||
.get("type")
|
||||
.context("Using default viewtype for ephemeral handling.")
|
||||
.log_err(context)
|
||||
.unwrap_or_default();
|
||||
let location_id: u32 = row.get("location_id")?;
|
||||
Ok((id, chat_id, viewtype, location_id))
|
||||
},
|
||||
@@ -436,7 +445,11 @@ WHERE
|
||||
|row| {
|
||||
let id: MsgId = row.get("id")?;
|
||||
let chat_id: ChatId = row.get("chat_id")?;
|
||||
let viewtype: Viewtype = row.get("type")?;
|
||||
let viewtype: Viewtype = row
|
||||
.get("type")
|
||||
.context("Using default viewtype for delete-old handling.")
|
||||
.log_err(context)
|
||||
.unwrap_or_default();
|
||||
let location_id: u32 = row.get("location_id")?;
|
||||
Ok((id, chat_id, viewtype, location_id))
|
||||
},
|
||||
|
||||
@@ -128,31 +128,33 @@ 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 alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.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.ctx, Timer::Enabled { duration: 60 })
|
||||
.set_ephemeral_timer(alice, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&sent).await;
|
||||
let bob_received_message = bob.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
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?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
chat_alice
|
||||
.set_ephemeral_timer(&alice.ctx, Timer::Disabled)
|
||||
.set_ephemeral_timer(alice, Timer::Disabled)
|
||||
.await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Disabled
|
||||
);
|
||||
assert_eq!(chat_bob.get_ephemeral_timer(bob).await?, Timer::Disabled);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::path::PathBuf;
|
||||
|
||||
use crate::chat::ChatId;
|
||||
use crate::config::Config;
|
||||
use crate::constants::Chattype;
|
||||
use crate::contact::ContactId;
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::message::MsgId;
|
||||
@@ -272,11 +273,13 @@ pub enum EventType {
|
||||
/// ID of the contact that wants to join.
|
||||
contact_id: ContactId,
|
||||
|
||||
/// Progress as:
|
||||
/// 300=vg-/vc-request received, typically shown as "bob@addr joins".
|
||||
/// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
|
||||
/// 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol.
|
||||
/// 1000=Protocol finished for this contact.
|
||||
/// ID of the chat in case of success.
|
||||
chat_id: ChatId,
|
||||
|
||||
/// The type of the joined chat.
|
||||
chat_type: Chattype,
|
||||
|
||||
/// Progress, always 1000.
|
||||
progress: usize,
|
||||
},
|
||||
|
||||
@@ -376,6 +379,44 @@ pub enum EventType {
|
||||
/// This event is emitted from the account whose property changed.
|
||||
AccountsItemChanged,
|
||||
|
||||
/// Incoming call.
|
||||
IncomingCall {
|
||||
/// ID of the message referring to the call.
|
||||
msg_id: MsgId,
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: ChatId,
|
||||
/// User-defined info as passed to place_outgoing_call()
|
||||
place_call_info: String,
|
||||
/// True if incoming call is a video call.
|
||||
has_video: bool,
|
||||
},
|
||||
|
||||
/// Incoming call accepted.
|
||||
IncomingCallAccepted {
|
||||
/// ID of the message referring to the call.
|
||||
msg_id: MsgId,
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: ChatId,
|
||||
},
|
||||
|
||||
/// Outgoing call accepted.
|
||||
OutgoingCallAccepted {
|
||||
/// ID of the message referring to the call.
|
||||
msg_id: MsgId,
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: ChatId,
|
||||
/// User-defined info as passed to accept_incoming_call()
|
||||
accept_call_info: String,
|
||||
},
|
||||
|
||||
/// Call ended.
|
||||
CallEnded {
|
||||
/// ID of the message referring to the call.
|
||||
msg_id: MsgId,
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: ChatId,
|
||||
},
|
||||
|
||||
/// Event for using in tests, e.g. as a fence between normally generated events.
|
||||
#[cfg(test)]
|
||||
Test,
|
||||
|
||||
@@ -86,6 +86,7 @@ pub enum HeaderDef {
|
||||
|
||||
ChatDispositionNotificationTo,
|
||||
ChatWebrtcRoom,
|
||||
ChatWebrtcAccepted,
|
||||
|
||||
/// This message deletes the messages listed in the value by rfc724_mid.
|
||||
ChatDelete,
|
||||
@@ -118,6 +119,11 @@ pub enum HeaderDef {
|
||||
AuthenticationResults,
|
||||
|
||||
/// Node address from iroh where direct addresses have been removed.
|
||||
///
|
||||
/// The node address sent in this header must have
|
||||
/// a non-null relay URL as contacting home relay
|
||||
/// is the only way to reach the node without
|
||||
/// direct addresses and global discovery.
|
||||
IrohNodeAddr,
|
||||
|
||||
/// Advertised gossip topic for one webxdc.
|
||||
|
||||
343
src/imap.rs
343
src/imap.rs
@@ -14,16 +14,17 @@ use std::{
|
||||
};
|
||||
|
||||
use anyhow::{Context as _, Result, bail, ensure, format_err};
|
||||
use async_channel::Receiver;
|
||||
use async_channel::{self, Receiver, Sender};
|
||||
use async_imap::types::{Fetch, Flag, Name, NameAttribute, UnsolicitedResponse};
|
||||
use deltachat_contact_tools::ContactAddress;
|
||||
use futures::{FutureExt as _, StreamExt, TryStreamExt};
|
||||
use futures::{FutureExt as _, TryStreamExt};
|
||||
use futures_lite::FutureExt;
|
||||
use num_traits::FromPrimitive;
|
||||
use rand::Rng;
|
||||
use ratelimit::Ratelimit;
|
||||
use url::Url;
|
||||
|
||||
use crate::calls::{create_fallback_ice_servers, create_ice_servers_from_metadata};
|
||||
use crate::chat::{self, ChatId, ChatIdBlocked};
|
||||
use crate::chatlist_events;
|
||||
use crate::config::Config;
|
||||
@@ -47,7 +48,7 @@ use crate::receive_imf::{
|
||||
};
|
||||
use crate::scheduler::connectivity::ConnectivityStore;
|
||||
use crate::stock_str;
|
||||
use crate::tools::{self, create_id, duration_to_str};
|
||||
use crate::tools::{self, create_id, duration_to_str, time};
|
||||
|
||||
pub(crate) mod capabilities;
|
||||
mod client;
|
||||
@@ -123,6 +124,18 @@ pub(crate) struct ServerMetadata {
|
||||
pub admin: Option<String>,
|
||||
|
||||
pub iroh_relay: Option<Url>,
|
||||
|
||||
/// JSON with ICE servers for WebRTC calls
|
||||
/// and the expiration timestamp.
|
||||
///
|
||||
/// If JSON is about to expire, new TURN credentials
|
||||
/// should be fetched from the server
|
||||
/// to be ready for WebRTC calls.
|
||||
pub ice_servers: String,
|
||||
|
||||
/// Timestamp when ICE servers are considered
|
||||
/// expired and should be updated.
|
||||
pub ice_servers_expiration_timestamp: i64,
|
||||
}
|
||||
|
||||
impl async_imap::Authenticator for OAuth2 {
|
||||
@@ -146,7 +159,6 @@ pub enum FolderMeaning {
|
||||
Mvbox,
|
||||
Sent,
|
||||
Trash,
|
||||
Drafts,
|
||||
|
||||
/// Virtual folders.
|
||||
///
|
||||
@@ -166,7 +178,6 @@ impl FolderMeaning {
|
||||
FolderMeaning::Mvbox => Some(Config::ConfiguredMvboxFolder),
|
||||
FolderMeaning::Sent => Some(Config::ConfiguredSentboxFolder),
|
||||
FolderMeaning::Trash => Some(Config::ConfiguredTrashFolder),
|
||||
FolderMeaning::Drafts => None,
|
||||
FolderMeaning::Virtual => None,
|
||||
}
|
||||
}
|
||||
@@ -325,7 +336,7 @@ impl Imap {
|
||||
}
|
||||
|
||||
info!(context, "Connecting to IMAP server.");
|
||||
self.connectivity.set_connecting(context).await;
|
||||
self.connectivity.set_connecting(context);
|
||||
|
||||
self.conn_last_try = tools::Time::now();
|
||||
const BACKOFF_MIN_MS: u64 = 2000;
|
||||
@@ -408,7 +419,7 @@ impl Imap {
|
||||
"IMAP-LOGIN as {}",
|
||||
lp.user
|
||||
)));
|
||||
self.connectivity.set_preparing(context).await;
|
||||
self.connectivity.set_preparing(context);
|
||||
info!(context, "Successfully logged into IMAP server.");
|
||||
return Ok(session);
|
||||
}
|
||||
@@ -466,7 +477,7 @@ impl Imap {
|
||||
let mut session = match self.connect(context, configuring).await {
|
||||
Ok(session) => session,
|
||||
Err(err) => {
|
||||
self.connectivity.set_err(context, &err).await;
|
||||
self.connectivity.set_err(context, &err);
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
@@ -555,14 +566,42 @@ impl Imap {
|
||||
}
|
||||
session.new_mail = false;
|
||||
|
||||
let mut read_cnt = 0;
|
||||
loop {
|
||||
let (n, fetch_more) = self
|
||||
.fetch_new_msg_batch(context, session, folder, folder_meaning)
|
||||
.await?;
|
||||
read_cnt += n;
|
||||
if !fetch_more {
|
||||
return Ok(read_cnt > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns number of messages processed and whether the function should be called again.
|
||||
async fn fetch_new_msg_batch(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
session: &mut Session,
|
||||
folder: &str,
|
||||
folder_meaning: FolderMeaning,
|
||||
) -> Result<(usize, bool)> {
|
||||
let uid_validity = get_uidvalidity(context, folder).await?;
|
||||
let old_uid_next = get_uid_next(context, folder).await?;
|
||||
info!(
|
||||
context,
|
||||
"fetch_new_msg_batch({folder}): UIDVALIDITY={uid_validity}, UIDNEXT={old_uid_next}."
|
||||
);
|
||||
|
||||
let msgs = session.prefetch(old_uid_next).await.context("prefetch")?;
|
||||
let uids_to_prefetch = 500;
|
||||
let msgs = session
|
||||
.prefetch(old_uid_next, uids_to_prefetch)
|
||||
.await
|
||||
.context("prefetch")?;
|
||||
let read_cnt = msgs.len();
|
||||
|
||||
let download_limit = context.download_limit().await?;
|
||||
let mut uids_fetch = Vec::<(_, bool /* partially? */)>::with_capacity(msgs.len() + 1);
|
||||
let mut uids_fetch = Vec::<(u32, bool /* partially? */)>::with_capacity(msgs.len() + 1);
|
||||
let mut uid_message_ids = BTreeMap::new();
|
||||
let mut largest_uid_skipped = None;
|
||||
let delete_target = context.get_delete_msgs_target().await?;
|
||||
@@ -692,54 +731,79 @@ impl Imap {
|
||||
}
|
||||
|
||||
if !uids_fetch.is_empty() {
|
||||
self.connectivity.set_working(context).await;
|
||||
self.connectivity.set_working(context);
|
||||
}
|
||||
|
||||
// Actually download messages.
|
||||
let mut largest_uid_fetched: u32 = 0;
|
||||
let (sender, receiver) = async_channel::unbounded();
|
||||
|
||||
let mut received_msgs = Vec::with_capacity(uids_fetch.len());
|
||||
let mut uids_fetch_in_batch = Vec::with_capacity(max(uids_fetch.len(), 1));
|
||||
let mut fetch_partially = false;
|
||||
uids_fetch.push((0, !uids_fetch.last().unwrap_or(&(0, false)).1));
|
||||
for (uid, fp) in uids_fetch {
|
||||
if fp != fetch_partially {
|
||||
let (largest_uid_fetched_in_batch, received_msgs_in_batch) = session
|
||||
.fetch_many_msgs(
|
||||
context,
|
||||
folder,
|
||||
uid_validity,
|
||||
uids_fetch_in_batch.split_off(0),
|
||||
&uid_message_ids,
|
||||
fetch_partially,
|
||||
)
|
||||
.await
|
||||
.context("fetch_many_msgs")?;
|
||||
received_msgs.extend(received_msgs_in_batch);
|
||||
largest_uid_fetched = max(
|
||||
largest_uid_fetched,
|
||||
largest_uid_fetched_in_batch.unwrap_or(0),
|
||||
);
|
||||
fetch_partially = fp;
|
||||
}
|
||||
uids_fetch_in_batch.push(uid);
|
||||
}
|
||||
|
||||
// Advance uid_next to the maximum of the largest known UID plus 1
|
||||
// and mailbox UIDNEXT.
|
||||
// Largest known UID is normally less than UIDNEXT,
|
||||
// but a message may have arrived between determining UIDNEXT
|
||||
// and executing the FETCH command.
|
||||
let mailbox_uid_next = session
|
||||
.selected_mailbox
|
||||
.as_ref()
|
||||
.with_context(|| format!("Expected {folder:?} to be selected"))?
|
||||
.uid_next
|
||||
.unwrap_or_default();
|
||||
let new_uid_next = max(
|
||||
max(largest_uid_fetched, largest_uid_skipped.unwrap_or(0)) + 1,
|
||||
mailbox_uid_next,
|
||||
);
|
||||
|
||||
let update_uids_future = async {
|
||||
let mut largest_uid_fetched: u32 = 0;
|
||||
|
||||
while let Ok((uid, received_msg_opt)) = receiver.recv().await {
|
||||
largest_uid_fetched = max(largest_uid_fetched, uid);
|
||||
if let Some(received_msg) = received_msg_opt {
|
||||
received_msgs.push(received_msg)
|
||||
}
|
||||
}
|
||||
|
||||
largest_uid_fetched
|
||||
};
|
||||
|
||||
let actually_download_messages_future = async {
|
||||
let sender = sender;
|
||||
let mut uids_fetch_in_batch = Vec::with_capacity(max(uids_fetch.len(), 1));
|
||||
let mut fetch_partially = false;
|
||||
uids_fetch.push((0, !uids_fetch.last().unwrap_or(&(0, false)).1));
|
||||
for (uid, fp) in uids_fetch {
|
||||
if fp != fetch_partially {
|
||||
session
|
||||
.fetch_many_msgs(
|
||||
context,
|
||||
folder,
|
||||
uid_validity,
|
||||
uids_fetch_in_batch.split_off(0),
|
||||
&uid_message_ids,
|
||||
fetch_partially,
|
||||
sender.clone(),
|
||||
)
|
||||
.await
|
||||
.context("fetch_many_msgs")?;
|
||||
fetch_partially = fp;
|
||||
}
|
||||
uids_fetch_in_batch.push(uid);
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
};
|
||||
|
||||
let (largest_uid_fetched, fetch_res) =
|
||||
tokio::join!(update_uids_future, actually_download_messages_future);
|
||||
|
||||
// Advance uid_next to the largest fetched UID plus 1.
|
||||
//
|
||||
// This may be larger than `mailbox_uid_next`
|
||||
// if the message has arrived after selecting mailbox
|
||||
// and determining its UIDNEXT and before prefetch.
|
||||
let mut new_uid_next = largest_uid_fetched + 1;
|
||||
let fetch_more = fetch_res.is_ok() && {
|
||||
let prefetch_uid_next = old_uid_next + uids_to_prefetch;
|
||||
// If we have successfully fetched all messages we planned during prefetch,
|
||||
// then we have covered at least the range between old UIDNEXT
|
||||
// and UIDNEXT of the mailbox at the time of selecting it.
|
||||
new_uid_next = max(new_uid_next, min(prefetch_uid_next, mailbox_uid_next));
|
||||
|
||||
new_uid_next = max(new_uid_next, largest_uid_skipped.unwrap_or(0) + 1);
|
||||
|
||||
prefetch_uid_next < mailbox_uid_next
|
||||
};
|
||||
if new_uid_next > old_uid_next {
|
||||
set_uid_next(context, folder, new_uid_next).await?;
|
||||
}
|
||||
@@ -752,7 +816,11 @@ impl Imap {
|
||||
|
||||
chat::mark_old_messages_as_noticed(context, received_msgs).await?;
|
||||
|
||||
Ok(read_cnt > 0)
|
||||
// Now fail if fetching failed, so we will
|
||||
// establish a new session if this one is broken.
|
||||
fetch_res?;
|
||||
|
||||
Ok((read_cnt, fetch_more))
|
||||
}
|
||||
|
||||
/// Read the recipients from old emails sent by the user and add them as contacts.
|
||||
@@ -789,7 +857,10 @@ impl Session {
|
||||
.context("listing folders for resync")?;
|
||||
for folder in all_folders {
|
||||
let folder_meaning = get_folder_meaning(&folder);
|
||||
if folder_meaning != FolderMeaning::Virtual {
|
||||
if !matches!(
|
||||
folder_meaning,
|
||||
FolderMeaning::Virtual | FolderMeaning::Unknown
|
||||
) {
|
||||
self.resync_folder_uids(context, folder.name(), folder_meaning)
|
||||
.await?;
|
||||
}
|
||||
@@ -1300,9 +1371,19 @@ impl Session {
|
||||
|
||||
/// Fetches a list of messages by server UID.
|
||||
///
|
||||
/// Returns the last UID fetched successfully and the info about each downloaded message.
|
||||
/// Sends pairs of UID and info about each downloaded message to the provided channel.
|
||||
/// Received message info is optional because UID may be ignored
|
||||
/// if the message has a `\Deleted` flag.
|
||||
///
|
||||
/// The channel is used to return the results because the function may fail
|
||||
/// due to network errors before it finishes fetching all the messages.
|
||||
/// In this case caller still may want to process all the results
|
||||
/// received over the channel and persist last seen UID in the database
|
||||
/// before bubbling up the failure.
|
||||
///
|
||||
/// If the message is incorrect or there is a failure to write a message to the database,
|
||||
/// it is skipped and the error is logged.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
pub(crate) async fn fetch_many_msgs(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
@@ -1311,12 +1392,10 @@ impl Session {
|
||||
request_uids: Vec<u32>,
|
||||
uid_message_ids: &BTreeMap<u32, String>,
|
||||
fetch_partially: bool,
|
||||
) -> Result<(Option<u32>, Vec<ReceivedMsg>)> {
|
||||
let mut last_uid = None;
|
||||
let mut received_msgs = Vec::new();
|
||||
|
||||
received_msgs_channel: Sender<(u32, Option<ReceivedMsg>)>,
|
||||
) -> Result<()> {
|
||||
if request_uids.is_empty() {
|
||||
return Ok((last_uid, received_msgs));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for (request_uids, set) in build_sequence_sets(&request_uids)? {
|
||||
@@ -1351,14 +1430,15 @@ impl Session {
|
||||
|
||||
// Try to find a requested UID in returned FETCH responses.
|
||||
while fetch_response.is_none() {
|
||||
let Some(next_fetch_response) = fetch_responses.next().await else {
|
||||
let Some(next_fetch_response) = fetch_responses
|
||||
.try_next()
|
||||
.await
|
||||
.context("Failed to process IMAP FETCH result")?
|
||||
else {
|
||||
// No more FETCH responses received from the server.
|
||||
break;
|
||||
};
|
||||
|
||||
let next_fetch_response =
|
||||
next_fetch_response.context("Failed to process IMAP FETCH result")?;
|
||||
|
||||
if let Some(next_uid) = next_fetch_response.uid {
|
||||
if next_uid == request_uid {
|
||||
fetch_response = Some(next_fetch_response);
|
||||
@@ -1402,7 +1482,7 @@ impl Session {
|
||||
|
||||
if is_deleted {
|
||||
info!(context, "Not processing deleted msg {}.", request_uid);
|
||||
last_uid = Some(request_uid);
|
||||
received_msgs_channel.send((request_uid, None)).await?;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1413,7 +1493,7 @@ impl Session {
|
||||
context,
|
||||
"Not processing message {} without a BODY.", request_uid
|
||||
);
|
||||
last_uid = Some(request_uid);
|
||||
received_msgs_channel.send((request_uid, None)).await?;
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -1432,7 +1512,7 @@ impl Session {
|
||||
context,
|
||||
"Passing message UID {} to receive_imf().", request_uid
|
||||
);
|
||||
match receive_imf_inner(
|
||||
let res = receive_imf_inner(
|
||||
context,
|
||||
folder,
|
||||
uidvalidity,
|
||||
@@ -1440,25 +1520,45 @@ impl Session {
|
||||
rfc724_mid,
|
||||
body,
|
||||
is_seen,
|
||||
partial,
|
||||
partial.map(|msg_size| (msg_size, None)),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(received_msg) => {
|
||||
if let Some(m) = received_msg {
|
||||
received_msgs.push(m);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "receive_imf error: {:#}.", err);
|
||||
.await;
|
||||
let received_msg = if let Err(err) = res {
|
||||
warn!(context, "receive_imf error: {:#}.", err);
|
||||
if partial.is_some() {
|
||||
return Err(err);
|
||||
}
|
||||
receive_imf_inner(
|
||||
context,
|
||||
folder,
|
||||
uidvalidity,
|
||||
request_uid,
|
||||
rfc724_mid,
|
||||
body,
|
||||
is_seen,
|
||||
Some((body.len().try_into()?, Some(format!("{err:#}")))),
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
res?
|
||||
};
|
||||
last_uid = Some(request_uid)
|
||||
received_msgs_channel
|
||||
.send((request_uid, received_msg))
|
||||
.await?;
|
||||
}
|
||||
|
||||
// If we don't process the whole response, IMAP client is left in a broken state where
|
||||
// it will try to process the rest of response as the next response.
|
||||
while fetch_responses.next().await.is_some() {}
|
||||
//
|
||||
// Make sure to not ignore the errors, because
|
||||
// if connection times out, it will return
|
||||
// infinite stream of `Some(Err(_))` results.
|
||||
while fetch_responses
|
||||
.try_next()
|
||||
.await
|
||||
.context("Failed to drain FETCH responses")?
|
||||
.is_some()
|
||||
{}
|
||||
|
||||
if count != request_uids.len() {
|
||||
warn!(
|
||||
@@ -1477,7 +1577,7 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
Ok((last_uid, received_msgs))
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieves server metadata if it is supported.
|
||||
@@ -1491,7 +1591,43 @@ impl Session {
|
||||
}
|
||||
|
||||
let mut lock = context.metadata.write().await;
|
||||
if (*lock).is_some() {
|
||||
if let Some(ref mut old_metadata) = *lock {
|
||||
let now = time();
|
||||
|
||||
// Refresh TURN server credentials if they expire in 12 hours.
|
||||
if now + 3600 * 12 < old_metadata.ice_servers_expiration_timestamp {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!(context, "ICE servers expired, requesting new credentials.");
|
||||
let mailbox = "";
|
||||
let options = "";
|
||||
let metadata = self
|
||||
.get_metadata(mailbox, options, "(/shared/vendor/deltachat/turn)")
|
||||
.await?;
|
||||
let mut got_turn_server = false;
|
||||
for m in metadata {
|
||||
if m.entry == "/shared/vendor/deltachat/turn" {
|
||||
if let Some(value) = m.value {
|
||||
match create_ice_servers_from_metadata(context, &value).await {
|
||||
Ok((parsed_timestamp, parsed_ice_servers)) => {
|
||||
old_metadata.ice_servers_expiration_timestamp = parsed_timestamp;
|
||||
old_metadata.ice_servers = parsed_ice_servers;
|
||||
got_turn_server = false;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Failed to parse TURN server metadata: {err:#}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !got_turn_server {
|
||||
// Set expiration timestamp 7 days in the future so we don't request it again.
|
||||
old_metadata.ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
|
||||
old_metadata.ice_servers = create_fallback_ice_servers(context).await?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -1503,6 +1639,8 @@ impl Session {
|
||||
let mut comment = None;
|
||||
let mut admin = None;
|
||||
let mut iroh_relay = None;
|
||||
let mut ice_servers = None;
|
||||
let mut ice_servers_expiration_timestamp = 0;
|
||||
|
||||
let mailbox = "";
|
||||
let options = "";
|
||||
@@ -1510,7 +1648,7 @@ impl Session {
|
||||
.get_metadata(
|
||||
mailbox,
|
||||
options,
|
||||
"(/shared/comment /shared/admin /shared/vendor/deltachat/irohrelay)",
|
||||
"(/shared/comment /shared/admin /shared/vendor/deltachat/irohrelay /shared/vendor/deltachat/turn)",
|
||||
)
|
||||
.await?;
|
||||
for m in metadata {
|
||||
@@ -1533,13 +1671,36 @@ impl Session {
|
||||
}
|
||||
}
|
||||
}
|
||||
"/shared/vendor/deltachat/turn" => {
|
||||
if let Some(value) = m.value {
|
||||
match create_ice_servers_from_metadata(context, &value).await {
|
||||
Ok((parsed_timestamp, parsed_ice_servers)) => {
|
||||
ice_servers_expiration_timestamp = parsed_timestamp;
|
||||
ice_servers = Some(parsed_ice_servers);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Failed to parse TURN server metadata: {err:#}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let ice_servers = if let Some(ice_servers) = ice_servers {
|
||||
ice_servers
|
||||
} else {
|
||||
// Set expiration timestamp 7 days in the future so we don't request it again.
|
||||
ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
|
||||
create_fallback_ice_servers(context).await?
|
||||
};
|
||||
|
||||
*lock = Some(ServerMetadata {
|
||||
comment,
|
||||
admin,
|
||||
iroh_relay,
|
||||
ice_servers,
|
||||
ice_servers_expiration_timestamp,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
@@ -1655,7 +1816,7 @@ impl Session {
|
||||
.uid_store(uid_set, &query)
|
||||
.await
|
||||
.with_context(|| format!("IMAP failed to store: ({uid_set}, {query})"))?;
|
||||
while let Some(_response) = responses.next().await {
|
||||
while let Some(_response) = responses.try_next().await? {
|
||||
// Read all the responses
|
||||
}
|
||||
Ok(())
|
||||
@@ -2077,27 +2238,6 @@ fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
|
||||
"迷惑メール",
|
||||
"스팸",
|
||||
];
|
||||
const DRAFT_NAMES: &[&str] = &[
|
||||
"Drafts",
|
||||
"Kladder",
|
||||
"Entw?rfe",
|
||||
"Borradores",
|
||||
"Brouillons",
|
||||
"Bozze",
|
||||
"Concepten",
|
||||
"Wersje robocze",
|
||||
"Rascunhos",
|
||||
"Entwürfe",
|
||||
"Koncepty",
|
||||
"Kopie robocze",
|
||||
"Taslaklar",
|
||||
"Utkast",
|
||||
"Πρόχειρα",
|
||||
"Черновики",
|
||||
"下書き",
|
||||
"草稿",
|
||||
"임시보관함",
|
||||
];
|
||||
const TRASH_NAMES: &[&str] = &[
|
||||
"Trash",
|
||||
"Bin",
|
||||
@@ -2124,8 +2264,6 @@ fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
|
||||
FolderMeaning::Sent
|
||||
} else if SPAM_NAMES.iter().any(|s| s.to_lowercase() == lower) {
|
||||
FolderMeaning::Spam
|
||||
} else if DRAFT_NAMES.iter().any(|s| s.to_lowercase() == lower) {
|
||||
FolderMeaning::Drafts
|
||||
} else if TRASH_NAMES.iter().any(|s| s.to_lowercase() == lower) {
|
||||
FolderMeaning::Trash
|
||||
} else {
|
||||
@@ -2139,7 +2277,6 @@ fn get_folder_meaning_by_attrs(folder_attrs: &[NameAttribute]) -> FolderMeaning
|
||||
NameAttribute::Trash => return FolderMeaning::Trash,
|
||||
NameAttribute::Sent => return FolderMeaning::Sent,
|
||||
NameAttribute::Junk => return FolderMeaning::Spam,
|
||||
NameAttribute::Drafts => return FolderMeaning::Drafts,
|
||||
NameAttribute::All | NameAttribute::Flagged => return FolderMeaning::Virtual,
|
||||
NameAttribute::Extension(label) => {
|
||||
match label.as_ref() {
|
||||
|
||||
@@ -8,15 +8,13 @@ use tokio::io::BufWriter;
|
||||
|
||||
use super::capabilities::Capabilities;
|
||||
use crate::context::Context;
|
||||
use crate::log::{info, warn};
|
||||
use crate::log::{LoggingStream, info, warn};
|
||||
use crate::login_param::{ConnectionCandidate, ConnectionSecurity};
|
||||
use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp};
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::net::session::SessionStream;
|
||||
use crate::net::tls::wrap_tls;
|
||||
use crate::net::{
|
||||
connect_tcp_inner, connect_tls_inner, run_connection_attempts, update_connection_history,
|
||||
};
|
||||
use crate::net::{connect_tcp_inner, run_connection_attempts, update_connection_history};
|
||||
use crate::tools::time;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -126,12 +124,12 @@ impl Client {
|
||||
);
|
||||
let res = match security {
|
||||
ConnectionSecurity::Tls => {
|
||||
Client::connect_secure(resolved_addr, host, strict_tls).await
|
||||
Client::connect_secure(context, resolved_addr, host, strict_tls).await
|
||||
}
|
||||
ConnectionSecurity::Starttls => {
|
||||
Client::connect_starttls(resolved_addr, host, strict_tls).await
|
||||
Client::connect_starttls(context, resolved_addr, host, strict_tls).await
|
||||
}
|
||||
ConnectionSecurity::Plain => Client::connect_insecure(resolved_addr).await,
|
||||
ConnectionSecurity::Plain => Client::connect_insecure(context, resolved_addr).await,
|
||||
};
|
||||
match res {
|
||||
Ok(client) => {
|
||||
@@ -202,40 +200,61 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect_secure(addr: SocketAddr, hostname: &str, strict_tls: bool) -> Result<Self> {
|
||||
let tls_stream = connect_tls_inner(addr, hostname, strict_tls, alpn(addr.port())).await?;
|
||||
async fn connect_secure(
|
||||
context: &Context,
|
||||
addr: SocketAddr,
|
||||
hostname: &str,
|
||||
strict_tls: bool,
|
||||
) -> Result<Self> {
|
||||
let tcp_stream = connect_tcp_inner(addr).await?;
|
||||
let account_id = context.get_id();
|
||||
let events = context.events.clone();
|
||||
let logging_stream = LoggingStream::new(tcp_stream, account_id, events)?;
|
||||
let tls_stream = wrap_tls(strict_tls, hostname, alpn(addr.port()), logging_stream).await?;
|
||||
let buffered_stream = BufWriter::new(tls_stream);
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
|
||||
let mut client = Client::new(session_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await
|
||||
.context("failed to read greeting")??;
|
||||
.await?
|
||||
.context("Failed to read greeting")?;
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
async fn connect_insecure(addr: SocketAddr) -> Result<Self> {
|
||||
async fn connect_insecure(context: &Context, addr: SocketAddr) -> Result<Self> {
|
||||
let tcp_stream = connect_tcp_inner(addr).await?;
|
||||
let buffered_stream = BufWriter::new(tcp_stream);
|
||||
let account_id = context.get_id();
|
||||
let events = context.events.clone();
|
||||
let logging_stream = LoggingStream::new(tcp_stream, account_id, events)?;
|
||||
let buffered_stream = BufWriter::new(logging_stream);
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
|
||||
let mut client = Client::new(session_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await
|
||||
.context("failed to read greeting")??;
|
||||
.await?
|
||||
.context("Failed to read greeting")?;
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
async fn connect_starttls(addr: SocketAddr, host: &str, strict_tls: bool) -> Result<Self> {
|
||||
async fn connect_starttls(
|
||||
context: &Context,
|
||||
addr: SocketAddr,
|
||||
host: &str,
|
||||
strict_tls: bool,
|
||||
) -> Result<Self> {
|
||||
let tcp_stream = connect_tcp_inner(addr).await?;
|
||||
|
||||
let account_id = context.get_id();
|
||||
let events = context.events.clone();
|
||||
let tcp_stream = LoggingStream::new(tcp_stream, account_id, events)?;
|
||||
|
||||
// Run STARTTLS command and convert the client back into a stream.
|
||||
let buffered_tcp_stream = BufWriter::new(tcp_stream);
|
||||
let mut client = async_imap::Client::new(buffered_tcp_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await
|
||||
.context("failed to read greeting")??;
|
||||
.await?
|
||||
.context("Failed to read greeting")?;
|
||||
client
|
||||
.run_command_and_check_ok("STARTTLS", None)
|
||||
.await
|
||||
@@ -246,7 +265,6 @@ impl Client {
|
||||
let tls_stream = wrap_tls(strict_tls, host, &[], tcp_stream)
|
||||
.await
|
||||
.context("STARTTLS upgrade failed")?;
|
||||
|
||||
let buffered_stream = BufWriter::new(tls_stream);
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
|
||||
let client = Client::new(session_stream);
|
||||
@@ -269,8 +287,8 @@ impl Client {
|
||||
let mut client = Client::new(session_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await
|
||||
.context("failed to read greeting")??;
|
||||
.await?
|
||||
.context("Failed to read greeting")?;
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
@@ -286,8 +304,8 @@ impl Client {
|
||||
let mut client = Client::new(session_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await
|
||||
.context("failed to read greeting")??;
|
||||
.await?
|
||||
.context("Failed to read greeting")?;
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
@@ -307,8 +325,8 @@ impl Client {
|
||||
let mut client = ImapClient::new(buffered_proxy_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await
|
||||
.context("failed to read greeting")??;
|
||||
.await?
|
||||
.context("Failed to read greeting")?;
|
||||
client
|
||||
.run_command_and_check_ok("STARTTLS", None)
|
||||
.await
|
||||
|
||||
@@ -73,8 +73,8 @@ impl Imap {
|
||||
|
||||
// Don't scan folders that are watched anyway
|
||||
if !watched_folders.contains(&folder.name().to_string())
|
||||
&& folder_meaning != FolderMeaning::Drafts
|
||||
&& folder_meaning != FolderMeaning::Trash
|
||||
&& folder_meaning != FolderMeaning::Unknown
|
||||
{
|
||||
self.fetch_move_delete(context, session, folder.name(), folder_meaning)
|
||||
.await
|
||||
|
||||
@@ -110,14 +110,16 @@ impl Session {
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
/// Prefetch all messages greater than or equal to `uid_next`. Returns a list of fetch results
|
||||
/// in the order of ascending delivery time to the server (INTERNALDATE).
|
||||
/// Prefetch `n_uids` messages starting from `uid_next`. Returns a list of fetch results in the
|
||||
/// order of ascending delivery time to the server (INTERNALDATE).
|
||||
pub(crate) async fn prefetch(
|
||||
&mut self,
|
||||
uid_next: u32,
|
||||
n_uids: u32,
|
||||
) -> Result<Vec<(u32, async_imap::types::Fetch)>> {
|
||||
let uid_last = uid_next.saturating_add(n_uids - 1);
|
||||
// fetch messages with larger UID than the last one seen
|
||||
let set = format!("{uid_next}:*");
|
||||
let set = format!("{uid_next}:{uid_last}");
|
||||
let mut list = self
|
||||
.uid_fetch(set, PREFETCH_FLAGS)
|
||||
.await
|
||||
@@ -126,16 +128,7 @@ impl Session {
|
||||
let mut msgs = BTreeMap::new();
|
||||
while let Some(msg) = list.try_next().await? {
|
||||
if let Some(msg_uid) = msg.uid {
|
||||
// If the mailbox is not empty, results always include
|
||||
// at least one UID, even if last_seen_uid+1 is past
|
||||
// the last UID in the mailbox. It happens because
|
||||
// uid:* is interpreted the same way as *:uid.
|
||||
// See <https://tools.ietf.org/html/rfc3501#page-61> for
|
||||
// standard reference. Therefore, sometimes we receive
|
||||
// already seen messages and have to filter them out.
|
||||
if msg_uid >= uid_next {
|
||||
msgs.insert((msg.internal_date(), msg_uid), msg);
|
||||
}
|
||||
msgs.insert((msg.internal_date(), msg_uid), msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
137
src/imex.rs
137
src/imex.rs
@@ -90,7 +90,7 @@ pub async fn imex(
|
||||
let cancel = context.alloc_ongoing().await?;
|
||||
|
||||
let res = {
|
||||
let _guard = context.scheduler.pause(context.clone()).await?;
|
||||
let _guard = context.scheduler.pause(context).await?;
|
||||
imex_inner(context, what, path, passphrase)
|
||||
.race(async {
|
||||
cancel.recv().await.ok();
|
||||
@@ -140,32 +140,8 @@ pub async fn has_backup(_context: &Context, dir_name: &Path) -> Result<String> {
|
||||
}
|
||||
|
||||
async fn set_self_key(context: &Context, armored: &str) -> Result<()> {
|
||||
// try hard to only modify key-state
|
||||
let (private_key, header) = SignedSecretKey::from_asc(armored)?;
|
||||
let private_key = SignedSecretKey::from_asc(armored)?;
|
||||
let public_key = private_key.split_public_key()?;
|
||||
if let Some(preferencrypt) = header.get("Autocrypt-Prefer-Encrypt") {
|
||||
let e2ee_enabled = match preferencrypt.as_str() {
|
||||
"nopreference" => 0,
|
||||
"mutual" => 1,
|
||||
_ => {
|
||||
bail!("invalid Autocrypt-Prefer-Encrypt header: {:?}", header);
|
||||
}
|
||||
};
|
||||
context
|
||||
.sql
|
||||
.set_raw_config_int("e2ee_enabled", e2ee_enabled)
|
||||
.await?;
|
||||
} else {
|
||||
// `Autocrypt-Prefer-Encrypt` is not included
|
||||
// in keys exported to file.
|
||||
//
|
||||
// `Autocrypt-Prefer-Encrypt` also SHOULD be sent
|
||||
// in Autocrypt Setup Message according to Autocrypt specification,
|
||||
// but K-9 6.802 does not include this header.
|
||||
//
|
||||
// We keep current setting in this case.
|
||||
info!(context, "No Autocrypt-Prefer-Encrypt header.");
|
||||
};
|
||||
|
||||
let keypair = pgp::KeyPair {
|
||||
public: public_key,
|
||||
@@ -804,7 +780,7 @@ async fn export_database(
|
||||
"UPDATE backup.config SET value='0' WHERE keyname='verified_one_on_one_chats';",
|
||||
[],
|
||||
)
|
||||
.ok(); // If verified_one_on_one_chats was not set, this errors, which we ignore
|
||||
.ok(); // Deprecated 2025-07. If verified_one_on_one_chats was not set, this errors, which we ignore
|
||||
conn.execute("DETACH DATABASE backup", [])
|
||||
.context("failed to detach backup database")?;
|
||||
res?;
|
||||
@@ -952,75 +928,56 @@ mod tests {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_export_and_import_backup() -> Result<()> {
|
||||
for set_verified_oneonone_chats in [true, false] {
|
||||
let backup_dir = tempfile::tempdir().unwrap();
|
||||
let backup_dir = tempfile::tempdir().unwrap();
|
||||
|
||||
let context1 = TestContext::new_alice().await;
|
||||
assert!(context1.is_configured().await?);
|
||||
if set_verified_oneonone_chats {
|
||||
context1
|
||||
.set_config_bool(Config::VerifiedOneOnOneChats, true)
|
||||
.await?;
|
||||
}
|
||||
let context1 = TestContext::new_alice().await;
|
||||
assert!(context1.is_configured().await?);
|
||||
|
||||
let context2 = TestContext::new().await;
|
||||
assert!(!context2.is_configured().await?);
|
||||
assert!(has_backup(&context2, backup_dir.path()).await.is_err());
|
||||
let context2 = TestContext::new().await;
|
||||
assert!(!context2.is_configured().await?);
|
||||
assert!(has_backup(&context2, backup_dir.path()).await.is_err());
|
||||
|
||||
// export from context1
|
||||
assert!(
|
||||
imex(&context1, ImexMode::ExportBackup, backup_dir.path(), None)
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
let _event = context1
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
|
||||
.await;
|
||||
|
||||
// import to context2
|
||||
let backup = has_backup(&context2, backup_dir.path()).await?;
|
||||
|
||||
// Import of unencrypted backup with incorrect "foobar" backup passphrase fails.
|
||||
assert!(
|
||||
imex(
|
||||
&context2,
|
||||
ImexMode::ImportBackup,
|
||||
backup.as_ref(),
|
||||
Some("foobar".to_string())
|
||||
)
|
||||
// export from context1
|
||||
assert!(
|
||||
imex(&context1, ImexMode::ExportBackup, backup_dir.path(), None)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
.is_ok()
|
||||
);
|
||||
let _event = context1
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
imex(&context2, ImexMode::ImportBackup, backup.as_ref(), None)
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
let _event = context2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
|
||||
.await;
|
||||
// import to context2
|
||||
let backup = has_backup(&context2, backup_dir.path()).await?;
|
||||
|
||||
assert!(context2.is_configured().await?);
|
||||
assert_eq!(
|
||||
context2.get_config(Config::Addr).await?,
|
||||
Some("alice@example.org".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
context2
|
||||
.get_config_bool(Config::VerifiedOneOnOneChats)
|
||||
.await?,
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
context1
|
||||
.get_config_bool(Config::VerifiedOneOnOneChats)
|
||||
.await?,
|
||||
set_verified_oneonone_chats
|
||||
);
|
||||
}
|
||||
// Import of unencrypted backup with incorrect "foobar" backup passphrase fails.
|
||||
assert!(
|
||||
imex(
|
||||
&context2,
|
||||
ImexMode::ImportBackup,
|
||||
backup.as_ref(),
|
||||
Some("foobar".to_string())
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
assert!(
|
||||
imex(&context2, ImexMode::ImportBackup, backup.as_ref(), None)
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
let _event = context2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
|
||||
.await;
|
||||
|
||||
assert!(context2.is_configured().await?);
|
||||
assert_eq!(
|
||||
context2.get_config(Config::Addr).await?,
|
||||
Some("alice@example.org".to_string())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -93,10 +93,7 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<St
|
||||
bail!("Passphrase must be at least 2 chars long.");
|
||||
};
|
||||
let private_key = load_self_secret_key(context).await?;
|
||||
let ac_headers = match context.get_config_bool(Config::E2eeEnabled).await? {
|
||||
false => None,
|
||||
true => Some(("Autocrypt-Prefer-Encrypt", "mutual")),
|
||||
};
|
||||
let ac_headers = Some(("Autocrypt-Prefer-Encrypt", "mutual"));
|
||||
let private_key_asc = private_key.to_asc(ac_headers);
|
||||
let encr = pgp::symm_encrypt(passphrase, private_key_asc.into_bytes())
|
||||
.await?
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user