mirror of
https://github.com/chatmail/core.git
synced 2026-04-17 05:26:42 +03:00
Compare commits
167 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bb0b86f6a | ||
|
|
ed2b0e8f03 | ||
|
|
8152ff518e | ||
|
|
cbcfb7087e | ||
|
|
396104af47 | ||
|
|
69f6727751 | ||
|
|
b72a677f4c | ||
|
|
00e78eecf6 | ||
|
|
8b0621b724 | ||
|
|
63bf4c4f33 | ||
|
|
d6bce56d18 | ||
|
|
c8dec0dcdd | ||
|
|
509644ea5f | ||
|
|
3e95239e71 | ||
|
|
74d4b823d2 | ||
|
|
1bcfb90b90 | ||
|
|
411ee511ed | ||
|
|
e5a30c341c | ||
|
|
3d409c37a1 | ||
|
|
b46c86c9b7 | ||
|
|
e5e268f503 | ||
|
|
633536bb13 | ||
|
|
94ee485155 | ||
|
|
ec0dc8bcad | ||
|
|
49296e3014 | ||
|
|
2b93e856e4 | ||
|
|
c5be7df1d7 | ||
|
|
6b74cb6539 | ||
|
|
de2ac8cca2 | ||
|
|
085fcd2751 | ||
|
|
83f30e4a54 | ||
|
|
e79b4baa09 | ||
|
|
1e0c0d8efa | ||
|
|
378fb09c80 | ||
|
|
ff2fbebff0 | ||
|
|
50a73666fd | ||
|
|
61a8eff2ad | ||
|
|
cbd379fdf0 | ||
|
|
fe826f762e | ||
|
|
2019debe99 | ||
|
|
6c4f4bfd19 | ||
|
|
44b0736216 | ||
|
|
3b29469102 | ||
|
|
6325a35b5b | ||
|
|
c08644490a | ||
|
|
955f79923a | ||
|
|
c9026bff2c | ||
|
|
4fc0d0f53d | ||
|
|
1bf24618fa | ||
|
|
3f98e45c29 | ||
|
|
26ddcfaaed | ||
|
|
f0a12d493c | ||
|
|
c848ea7eda | ||
|
|
7c55356271 | ||
|
|
f4ee01ecca | ||
|
|
448c0d2268 | ||
|
|
3325270896 | ||
|
|
b563064b26 | ||
|
|
8d32d3ae0c | ||
|
|
c5f19f67a9 | ||
|
|
baeb31b5fa | ||
|
|
5d3bc00fd5 | ||
|
|
424928b660 | ||
|
|
1b8c732611 | ||
|
|
2531dfea1d | ||
|
|
9003b248aa | ||
|
|
35875f9b32 | ||
|
|
008e6c4af3 | ||
|
|
a6baba1852 | ||
|
|
a6b2a54e46 | ||
|
|
99aa99eb5b | ||
|
|
566395f1fa | ||
|
|
4ccd3cb665 | ||
|
|
f5e1e2678b | ||
|
|
c3a5e3ac0d | ||
|
|
b2f31c8148 | ||
|
|
29c57ad065 | ||
|
|
82a0d6b0ab | ||
|
|
5ff323ce15 | ||
|
|
a67a5299bf | ||
|
|
659d21aa9d | ||
|
|
8f604e74ec | ||
|
|
e1ebf3e96d | ||
|
|
76171aea2e | ||
|
|
96b8d1720e | ||
|
|
47b49fd02e | ||
|
|
f50e3d6ffa | ||
|
|
2ecb537307 | ||
|
|
ccae73f6db | ||
|
|
fce91f3ee0 | ||
|
|
d446a16fc6 | ||
|
|
c3a6e48882 | ||
|
|
46ec3a469b | ||
|
|
fe3b1ea16d | ||
|
|
ed300b6f97 | ||
|
|
e456be4e21 | ||
|
|
ba4055b7df | ||
|
|
c06f53cb86 | ||
|
|
13dafa46b5 | ||
|
|
d552250dc4 | ||
|
|
1383e790c3 | ||
|
|
b536902827 | ||
|
|
2631745a57 | ||
|
|
46bbe5f077 | ||
|
|
0f14edd5d9 | ||
|
|
fe6e942191 | ||
|
|
67aac12995 | ||
|
|
f2fb59f0cc | ||
|
|
55ab1b86f7 | ||
|
|
ceba687df3 | ||
|
|
7e811469b3 | ||
|
|
cdacad235e | ||
|
|
c766397abc | ||
|
|
14a59afd5d | ||
|
|
9c883e6424 | ||
|
|
9d7db20225 | ||
|
|
fdb583b5e9 | ||
|
|
8d6f4b0354 | ||
|
|
284469363e | ||
|
|
6078c79020 | ||
|
|
161e5ae358 | ||
|
|
a66859ebf2 | ||
|
|
de902babc3 | ||
|
|
98a8679779 | ||
|
|
8ce3ecc809 | ||
|
|
50a1f907a5 | ||
|
|
c2bf2a32b5 | ||
|
|
84710384fe | ||
|
|
f60fce22ed | ||
|
|
0d2f2b3266 | ||
|
|
516f0a1a98 | ||
|
|
25750de4e1 | ||
|
|
a89ce8ce7a | ||
|
|
9ac64ea6b9 | ||
|
|
294e23d82d | ||
|
|
184736723f | ||
|
|
cea528ed61 | ||
|
|
9b11f53da6 | ||
|
|
5c339efb70 | ||
|
|
d71c163c7d | ||
|
|
19fde9594f | ||
|
|
20b3a06adf | ||
|
|
b0127fa381 | ||
|
|
6a293aebe2 | ||
|
|
fd90493766 | ||
|
|
b1883c802b | ||
|
|
71ee32b8b7 | ||
|
|
84161f4202 | ||
|
|
4af9463a91 | ||
|
|
ddd4fc49a2 | ||
|
|
7d5bedde4d | ||
|
|
e34fee72a0 | ||
|
|
7ba4a43253 | ||
|
|
a09fd4577a | ||
|
|
525a3539d2 | ||
|
|
fbcdd45015 | ||
|
|
1ea8ed6442 | ||
|
|
f6817131b8 | ||
|
|
28fc1d2ff2 | ||
|
|
5925f72316 | ||
|
|
8dfa5fc37e | ||
|
|
49b04e8789 | ||
|
|
d87d87f467 | ||
|
|
bf72b3ad49 | ||
|
|
30f2981259 | ||
|
|
121bfd1fa8 | ||
|
|
9e2a4325e9 |
28
.github/workflows/ci.yml
vendored
28
.github/workflows/ci.yml
vendored
@@ -20,7 +20,7 @@ permissions: {}
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
RUST_VERSION: 1.91.0
|
||||
RUST_VERSION: 1.93.0
|
||||
|
||||
# Minimum Supported Rust Version
|
||||
MSRV: 1.88.0
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
- run: rustup override set $RUST_VERSION
|
||||
shell: bash
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v2
|
||||
uses: swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
|
||||
- name: Run rustfmt
|
||||
run: cargo fmt --all -- --check
|
||||
- name: Run clippy
|
||||
@@ -59,9 +59,9 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
- uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979
|
||||
with:
|
||||
arguments: --all-features --workspace
|
||||
arguments: --workspace --all-features --locked
|
||||
command: check
|
||||
command-arguments: "-Dwarnings"
|
||||
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v2
|
||||
uses: swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
|
||||
- name: Rustdoc
|
||||
run: cargo doc --document-private-items --no-deps
|
||||
|
||||
@@ -134,10 +134,10 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v2
|
||||
uses: swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@v2
|
||||
uses: taiki-e/install-action@69e777b377e4ec209ddad9426ae3e0c1008b0ef3
|
||||
with:
|
||||
tool: nextest
|
||||
|
||||
@@ -168,13 +168,13 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v2
|
||||
uses: swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
|
||||
|
||||
- name: Build C library
|
||||
run: cargo build -p deltachat_ffi
|
||||
|
||||
- name: Upload C library
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ matrix.os }}-libdeltachat.a
|
||||
path: target/debug/libdeltachat.a
|
||||
@@ -194,13 +194,13 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v2
|
||||
uses: swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
|
||||
|
||||
- name: Build deltachat-rpc-server
|
||||
run: cargo build -p deltachat-rpc-server
|
||||
|
||||
- name: Upload deltachat-rpc-server
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ matrix.os }}-deltachat-rpc-server
|
||||
path: ${{ matrix.os == 'windows-latest' && 'target/debug/deltachat-rpc-server.exe' || 'target/debug/deltachat-rpc-server' }}
|
||||
@@ -243,7 +243,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download libdeltachat.a
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: ubuntu-latest-libdeltachat.a
|
||||
path: target/debug
|
||||
@@ -293,7 +293,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download libdeltachat.a
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.os }}-libdeltachat.a
|
||||
path: target/debug
|
||||
@@ -355,7 +355,7 @@ jobs:
|
||||
run: pip install tox
|
||||
|
||||
- name: Download deltachat-rpc-server
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.os }}-deltachat-rpc-server
|
||||
path: target/debug
|
||||
|
||||
94
.github/workflows/deltachat-rpc-server.yml
vendored
94
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -34,13 +34,13 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||
path: result/bin/deltachat-rpc-server
|
||||
@@ -58,13 +58,13 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
|
||||
|
||||
- name: Upload wheel
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
|
||||
path: result/*.whl
|
||||
@@ -82,13 +82,13 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}
|
||||
path: result/bin/deltachat-rpc-server.exe
|
||||
@@ -106,13 +106,13 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-wheel
|
||||
|
||||
- name: Upload wheel
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-wheel
|
||||
path: result/*.whl
|
||||
@@ -139,7 +139,7 @@ jobs:
|
||||
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.arch }}-apple-darwin --features vendored
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-macos
|
||||
path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server
|
||||
@@ -157,13 +157,13 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-android
|
||||
path: result/bin/deltachat-rpc-server
|
||||
@@ -181,13 +181,13 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android-wheel
|
||||
|
||||
- name: Upload wheel
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-android-wheel
|
||||
path: result/*.whl
|
||||
@@ -208,124 +208,124 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
|
||||
- name: Download Linux aarch64 binary
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-aarch64-linux
|
||||
path: deltachat-rpc-server-aarch64-linux.d
|
||||
|
||||
- name: Download Linux aarch64 wheel
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-aarch64-linux-wheel
|
||||
path: deltachat-rpc-server-aarch64-linux-wheel.d
|
||||
|
||||
- name: Download Linux armv7l binary
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-armv7l-linux
|
||||
path: deltachat-rpc-server-armv7l-linux.d
|
||||
|
||||
- name: Download Linux armv7l wheel
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-armv7l-linux-wheel
|
||||
path: deltachat-rpc-server-armv7l-linux-wheel.d
|
||||
|
||||
- name: Download Linux armv6l binary
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-armv6l-linux
|
||||
path: deltachat-rpc-server-armv6l-linux.d
|
||||
|
||||
- name: Download Linux armv6l wheel
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-armv6l-linux-wheel
|
||||
path: deltachat-rpc-server-armv6l-linux-wheel.d
|
||||
|
||||
- name: Download Linux i686 binary
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-i686-linux
|
||||
path: deltachat-rpc-server-i686-linux.d
|
||||
|
||||
- name: Download Linux i686 wheel
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-i686-linux-wheel
|
||||
path: deltachat-rpc-server-i686-linux-wheel.d
|
||||
|
||||
- name: Download Linux x86_64 binary
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64-linux
|
||||
path: deltachat-rpc-server-x86_64-linux.d
|
||||
|
||||
- name: Download Linux x86_64 wheel
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64-linux-wheel
|
||||
path: deltachat-rpc-server-x86_64-linux-wheel.d
|
||||
|
||||
- name: Download Win32 binary
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-win32
|
||||
path: deltachat-rpc-server-win32.d
|
||||
|
||||
- name: Download Win32 wheel
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-win32-wheel
|
||||
path: deltachat-rpc-server-win32-wheel.d
|
||||
|
||||
- name: Download Win64 binary
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-win64
|
||||
path: deltachat-rpc-server-win64.d
|
||||
|
||||
- name: Download Win64 wheel
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-win64-wheel
|
||||
path: deltachat-rpc-server-win64-wheel.d
|
||||
|
||||
- name: Download macOS binary for x86_64
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
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@v6
|
||||
uses: actions/download-artifact@v7
|
||||
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@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-arm64-v8a-android
|
||||
path: deltachat-rpc-server-arm64-v8a-android.d
|
||||
|
||||
- name: Download Android wheel for arm64-v8a
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-arm64-v8a-android-wheel
|
||||
path: deltachat-rpc-server-arm64-v8a-android-wheel.d
|
||||
|
||||
- name: Download Android binary for armeabi-v7a
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-armeabi-v7a-android
|
||||
path: deltachat-rpc-server-armeabi-v7a-android.d
|
||||
|
||||
- name: Download Android wheel for armeabi-v7a
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-armeabi-v7a-android-wheel
|
||||
path: deltachat-rpc-server-armeabi-v7a-android-wheel.d
|
||||
@@ -382,7 +382,7 @@ jobs:
|
||||
|
||||
- name: Publish deltachat-rpc-server to PyPI
|
||||
if: github.event_name == 'release'
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e
|
||||
|
||||
publish_npm_package:
|
||||
name: Build & Publish npm prebuilds and deltachat-rpc-server
|
||||
@@ -406,67 +406,67 @@ jobs:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Download Linux aarch64 binary
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-aarch64-linux
|
||||
path: deltachat-rpc-server-aarch64-linux.d
|
||||
|
||||
- name: Download Linux armv7l binary
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-armv7l-linux
|
||||
path: deltachat-rpc-server-armv7l-linux.d
|
||||
|
||||
- name: Download Linux armv6l binary
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-armv6l-linux
|
||||
path: deltachat-rpc-server-armv6l-linux.d
|
||||
|
||||
- name: Download Linux i686 binary
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-i686-linux
|
||||
path: deltachat-rpc-server-i686-linux.d
|
||||
|
||||
- name: Download Linux x86_64 binary
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64-linux
|
||||
path: deltachat-rpc-server-x86_64-linux.d
|
||||
|
||||
- name: Download Win32 binary
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-win32
|
||||
path: deltachat-rpc-server-win32.d
|
||||
|
||||
- name: Download Win64 binary
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-win64
|
||||
path: deltachat-rpc-server-win64.d
|
||||
|
||||
- name: Download macOS binary for x86_64
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
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@v6
|
||||
uses: actions/download-artifact@v7
|
||||
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@v6
|
||||
uses: actions/download-artifact@v7
|
||||
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@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-armeabi-v7a-android
|
||||
path: deltachat-rpc-server-armeabi-v7a-android.d
|
||||
@@ -496,7 +496,7 @@ jobs:
|
||||
ls -lah
|
||||
|
||||
- name: Upload to artifacts
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-npm-package
|
||||
path: deltachat-rpc-server/npm-package/*.tgz
|
||||
|
||||
2
.github/workflows/jsonrpc.yml
vendored
2
.github/workflows/jsonrpc.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
with:
|
||||
node-version: 18.x
|
||||
- name: Add Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
|
||||
- name: npm install
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm install
|
||||
|
||||
6
.github/workflows/nix.yml
vendored
6
.github/workflows/nix.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
- run: nix fmt flake.nix -- --check
|
||||
|
||||
build:
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
build-macos:
|
||||
@@ -105,5 +105,5 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
working-directory: deltachat-rpc-client
|
||||
run: python3 -m build
|
||||
- name: Store the distribution packages
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: python-package-distributions
|
||||
path: deltachat-rpc-client/dist/
|
||||
@@ -42,9 +42,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Download all the dists
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: python-package-distributions
|
||||
path: dist/
|
||||
- name: Publish deltachat-rpc-client to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e
|
||||
|
||||
4
.github/workflows/repl.yml
vendored
4
.github/workflows/repl.yml
vendored
@@ -18,11 +18,11 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
- name: Build
|
||||
run: nix build .#deltachat-repl-win64
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: repl.exe
|
||||
path: "result/bin/deltachat-repl.exe"
|
||||
|
||||
4
.github/workflows/upload-docs.yml
vendored
4
.github/workflows/upload-docs.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
- name: Build Python documentation
|
||||
run: nix build .#python-docs
|
||||
- name: Upload to py.delta.chat
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
- name: Build C documentation
|
||||
run: nix build .#docs
|
||||
- name: Upload to c.delta.chat
|
||||
|
||||
2
.github/workflows/zizmor-scan.yml
vendored
2
.github/workflows/zizmor-scan.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244
|
||||
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867
|
||||
|
||||
- name: Run zizmor
|
||||
run: uvx zizmor --format sarif . > results.sarif
|
||||
|
||||
302
CHANGELOG.md
302
CHANGELOG.md
@@ -1,5 +1,300 @@
|
||||
# Changelog
|
||||
|
||||
## [2.41.0] - 2026-02-06
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Do not require `ShowEmails` to be set to `All` for adding second relay.
|
||||
- Use different strings for audio and video calls.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Don't set download state to Failure if message is available on another Session's transport ([#7684](https://github.com/chatmail/core/pull/7684)).
|
||||
- Make use of call stock strings.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Bump `time` from 0.3.37 to 0.3.47.
|
||||
|
||||
## [2.40.0] - 2026-02-04
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Receive_imf: Log reasoning for chat assignment.
|
||||
- Use more fitting encryption info message.
|
||||
- Send Intended Recipient Fingerprint subpackets.
|
||||
- Trash messages with intended recipient fingerprints, but w/o our one included.
|
||||
- Do not collect email addresses from messages after configuration.
|
||||
- Add device message about legacy `mvbox_move`.
|
||||
- Never create IMAP folders.
|
||||
- Make summary for pre-messages look like summary for fully downloaded messages ([#7775](https://github.com/chatmail/core/pull/7775)).
|
||||
- Don't call `BlobObject::create_and_deduplicate()` when forwarding message to the same account.
|
||||
- Allow clients to specify whether a call has video initially or not ([#7740](https://github.com/chatmail/core/pull/7740)).
|
||||
- Do not load more than one own key from the keychain.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Cross-account forwarding of a message which `has_html()` ([#7791](https://github.com/chatmail/core/pull/7791)).
|
||||
- Make self-contact a key-contact even if key isn't generated yet.
|
||||
- `apply_group_changes()`: Check whether From is key-contact.
|
||||
- Don't add SELF to unencrypted chat created from encrypted message ([#7661](https://github.com/chatmail/core/pull/7661)).
|
||||
- Don't upscale images and test that image resolution isn't changed unnecessarily ([#7769](https://github.com/chatmail/core/pull/7769)).
|
||||
- Restart i/o when there are new transports in a sync message ([#7640](https://github.com/chatmail/core/pull/7640)).
|
||||
- `add_or_lookup_key_contacts*()`: Advance fingerprint_iter on invalid address.
|
||||
- `receive_imf`: Look up key contact by intended recipient fingerprint ([#7661](https://github.com/chatmail/core/pull/7661)).
|
||||
- Remove `Config::DeleteToTrash` and `Config::ConfiguredTrashFolder`.
|
||||
|
||||
### API-Changes
|
||||
|
||||
- jsonrpc(python): Process events forever by default.
|
||||
|
||||
### CI
|
||||
|
||||
- Make scripts/deny.sh test the locked version of dependencies.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove unneeded dbg! statements ([#7776](https://github.com/chatmail/core/pull/7776)).
|
||||
- Remove unused Context.is_inbox().
|
||||
- Rename lookup_key_contacts_by_address_list() to lookup_key_contacts_fallback_to_chat().
|
||||
- Mark `ProviderOptions` as `non_exhaustive`.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Update provider database.
|
||||
- cargo: Update `bytes` from 1.11.0 to 1.11.1.
|
||||
- cargo: Bump tokio from 1.48.0 to 1.49.0.
|
||||
- cargo: Bump tokio-util from 0.7.17 to 0.7.18.
|
||||
- cargo: Bump libc from 0.2.178 to 0.2.180.
|
||||
- cargo: Bump quote from 1.0.42 to 1.0.44.
|
||||
- cargo: Bump syn from 2.0.111 to 2.0.114.
|
||||
- cargo: Bump human-panic from 2.0.4 to 2.0.6.
|
||||
- cargo: Bump chrono from 0.4.42 to 0.4.43.
|
||||
- cargo: Bump data-encoding from 2.9.0 to 2.10.0.
|
||||
- cargo: Bump colorutils-rs from 0.7.5 to 0.7.6.
|
||||
- Update provider database.
|
||||
- cargo: Bump thiserror from 2.0.17 to 2.0.18.
|
||||
- deps: Bump EmbarkStudios/cargo-deny-action from 2.0.14 to 2.0.15.
|
||||
- Remove RUSTSEC-2026-0002 exception from deny.toml.
|
||||
- cargo: Bump tokio-stream from 0.1.17 to 0.1.18.
|
||||
- cargo: Bump toml from 0.9.10+spec-1.1.0 to 0.9.11+spec-1.1.0.
|
||||
- cargo: Bump serde_json from 1.0.148 to 1.0.149.
|
||||
- cargo: Bump uuid from 1.19.0 to 1.20.0.
|
||||
- cargo: Bump rustls-pki-types from 1.13.2 to 1.14.0.
|
||||
- cargo: Bump tracing-subscriber from 0.3.20 to 0.3.22.
|
||||
|
||||
### Tests
|
||||
|
||||
- 2nd device receives message via new primary transport.
|
||||
- Make `test_dont_move_sync_msgs` less flaky.
|
||||
- Encrypted incoming message goes to encrypted 1:1 chat even if references messages in ad-hoc group.
|
||||
- Message in blocked chat arrives as InSeen.
|
||||
- Set `mvbox_move` to 0 for test rust accounts.
|
||||
|
||||
## [2.39.0] - 2026-01-23
|
||||
|
||||
### CI
|
||||
|
||||
- Update Rust to 1.93.0.
|
||||
|
||||
### Documentation
|
||||
|
||||
- RELEASE.md: Push preparation commit to the main branch before tagging.
|
||||
- RELEASE.md: Add section about dealing with failed releases.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Forward message with file ([#7755](https://github.com/chatmail/core/pull/7755)).
|
||||
- Do not additionally reduce the resolution of images that fit into the resolution-limit and are larger than the file-size-limit ([#7760](https://github.com/chatmail/core/pull/7760)).
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Merge v2.38.0 into main branch.
|
||||
- Cleanup deprecated functions/defines ([#7763](https://github.com/chatmail/core/pull/7763)).
|
||||
|
||||
## [2.38.0] - 2026-01-22
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Jsonrpc: remove `contacts` from `FullChat`. To migrate load contacts on demand via `get_contacts_by_ids` using `FullChat.contactIds` ([#7282](https://github.com/chatmail/core/pull/7282)).
|
||||
- jsonrpc: Add run_until parameter for bots ([#7688](https://github.com/chatmail/core/pull/7688)).
|
||||
- rust, jsonrpc: Add `get_message_read_receipt_count` method ([#7732](https://github.com/chatmail/core/pull/7732)).
|
||||
- rust and jsonrpc: Marknoticed_all_chats method to mark all chats as notices, including muted ones. ([#7709](https://github.com/chatmail/core/pull/7709)).
|
||||
- Public re-export of Connectivity ([#7737](https://github.com/chatmail/core/pull/7737)).
|
||||
|
||||
### Documentation
|
||||
|
||||
- Fix chat types.
|
||||
- Set_config_from_qr() configures context for "DCACCOUNT:" and "DCLOGIN:" QRs ([#7450](https://github.com/chatmail/core/pull/7450)).
|
||||
- Fix formatting of `indoc!` link.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Pre-messages / next version of download on demand ([#7371](https://github.com/chatmail/core/pull/7371)).
|
||||
- Connectivity view: move quota up and combine with IMAP state. ([#7653](https://github.com/chatmail/core/pull/7653)).
|
||||
- Execute sync message before checking for primary transport update.
|
||||
- Disable partial search by contact address.
|
||||
- Don't put text into post-message ([#7714](https://github.com/chatmail/core/pull/7714)).
|
||||
- Don't scale up Origin of multiple and broadcast recipients when sending a message.
|
||||
- pgp: Use preferred hash algorithm for signing instead of hardcoded SHA256.
|
||||
- In teamprofiles, don't mark chat as read on outgoing message ([#7717](https://github.com/chatmail/core/pull/7717)).
|
||||
- Send and apply MDNs to self ([#7005](https://github.com/chatmail/core/pull/7005))
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not show contact address in message info ([#7695](https://github.com/chatmail/core/pull/7695)).
|
||||
- Take transport_id into account when marking messages with \Seen flags.
|
||||
- Send bcc-self messages to all own relays ([#7656](https://github.com/chatmail/core/pull/7656)).
|
||||
- Only emit TransportsModified if transports are really modified.
|
||||
- Logging errors in deltachat-rpc-server during startup ([#7707](https://github.com/chatmail/core/pull/7707)).
|
||||
- Use only lowercase letters for stats id ([#7700](https://github.com/chatmail/core/pull/7700)).
|
||||
- Hide incoming broadcasts in `DC_GCL_FOR_FORWARDING` ([#7726](https://github.com/chatmail/core/pull/7726)).
|
||||
- Do not resolve ICE server hostnames during IMAP loop.
|
||||
- More reliable parsing of `dclogin:` links with ip address as host ([#7734](https://github.com/chatmail/core/pull/7734)).
|
||||
- Don't remember old channel members in the database ([#7716](https://github.com/chatmail/core/pull/7716)).
|
||||
- Make it possible to leave and immediately delete a chat ([#7744](https://github.com/chatmail/core/pull/7744)).
|
||||
- Emit MsgsChanged instead of MsgsNoticed on self-MDN if chat still has fresh messages.
|
||||
- Prevent possible infinite loop with invalid `smtp` row ([#7746](https://github.com/chatmail/core/pull/7746)).
|
||||
- Sync broadcast subscribers list ([#7578](https://github.com/chatmail/core/pull/7578))
|
||||
|
||||
### Refactor
|
||||
|
||||
- Don't use `concat!` in sql statements ([#7720](https://github.com/chatmail/core/pull/7720)).
|
||||
|
||||
### Tests
|
||||
|
||||
- Port test_dont_move_sync_msgs to JSON-RPC ([#7676](https://github.com/chatmail/core/pull/7676)).
|
||||
- rpc-client: Replace remaining print()s with `logging` ([#6082](https://github.com/chatmail/core/pull/6082)).
|
||||
|
||||
## [2.37.0] - 2026-01-08
|
||||
|
||||
### API-Changes
|
||||
|
||||
- JSON-RPC API `get_all_ui_config_keys` to get all "ui.*" config keys ([#7579](https://github.com/chatmail/core/pull/7579)).
|
||||
- Add `who_can_call_me` config option.
|
||||
- cffi api to create account manager with existing events channel to see events emitted during startup. `dc_event_channel_new`, `dc_event_channel_unref`, `dc_event_channel_get_event_emitter` and `dc_accounts_new_with_event_channel` ([#7609](https://github.com/chatmail/core/pull/7609)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Config option to skip seen synchronization ([#7694](https://github.com/chatmail/core/pull/7694)).
|
||||
- More text instead of sender in channel summary.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not rely on Secure-Join header to detect {vc,vg}-request.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update instructions to UI where to display the address.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: bump rsa from 0.9.9 to 0.9.10.
|
||||
- Update lru 0.12.3 to 0.12.5 and add RUSTSEC-2026-0002 exception.
|
||||
|
||||
### Refactor
|
||||
|
||||
- ffi: Replace implicit drop in cffi with explicit `drop(Arc::from_raw(var))` ([#7664](https://github.com/chatmail/core/pull/7664)).
|
||||
|
||||
### Tests
|
||||
|
||||
- Regression test for vc-request encrypted by the server.
|
||||
- Test that channel summary does not have sender name.
|
||||
|
||||
## [2.36.0] - 2026-01-03
|
||||
|
||||
### CI
|
||||
|
||||
- Pin GitHub Action references.
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add transports event to FFI.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add core version to `receive_imf` failure message.
|
||||
- Connectivity view: quota for all transports ([#7630](https://github.com/chatmail/core/pull/7630)).
|
||||
- Send sync messages over SMTP and do not move them to mvbox.
|
||||
|
||||
### Fixes
|
||||
|
||||
- When accepting group, add members with `Origin::IncomingTo` and sort them down in the contact list (7592).
|
||||
- Update fallback welcome message.
|
||||
- `inner_configure`: Check Config::OnlyFetchMvbox before MvboxMove for multi-transport ([#7637](https://github.com/chatmail/core/pull/7637)).
|
||||
- Reset options not available for chatmail on chatmail profiles.
|
||||
- Don't send webxdc notification for `notify: "*"` when chat is muted ([#7658](https://github.com/chatmail/core/pull/7658)).
|
||||
|
||||
### Documentation
|
||||
|
||||
- `delete_chat()`: don't lie that messages aren't deleted from server.
|
||||
- Remove references to removed `sentbox_watch` config.
|
||||
- Update documentation for `TransportsModified` event.
|
||||
|
||||
### Tests
|
||||
|
||||
- Contact list after accepting group with unknown contacts ([#7592](https://github.com/chatmail/core/pull/7592)).
|
||||
- Port test_import_export_online_all to JSON-RPC ([#7411](https://github.com/chatmail/core/pull/7411)).
|
||||
|
||||
### Refactor
|
||||
|
||||
- Turn `DC_VERSION_STR` into `&str`.
|
||||
- ffi: Remove one pointer indirection for `dc_accounts_t`.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- deps: Bump actions/download-artifact from 6 to 7.
|
||||
- deps: Bump actions/upload-artifact from 5 to 6.
|
||||
- deps: Bump astral-sh/setup-uv from 7.1.4 to 7.1.6.
|
||||
- deps: Bump cachix/install-nix-action from 31.8.4 to 31.9.0.
|
||||
- cargo: Bump serde_json from 1.0.145 to 1.0.147.
|
||||
- cargo: Bump uuid from 1.18.1 to 1.19.0.
|
||||
- cargo: Bump toml from 0.9.8 to 0.9.10+spec-1.1.0.
|
||||
- cargo: Bump tempfile from 3.23.0 to 3.24.0.
|
||||
- cargo: Bump libc from 0.2.177 to 0.2.178.
|
||||
- cargo: Bump tracing from 0.1.41 to 0.1.44.
|
||||
- cargo: Bump hyper-util from 0.1.18 to 0.1.19.
|
||||
- cargo: Bump log from 0.4.28 to 0.4.29.
|
||||
- cargo: Bump rustls-pki-types from 1.13.0 to 1.13.2.
|
||||
- cargo: Bump criterion from 0.7.0 to 0.8.1.
|
||||
|
||||
## [2.35.0] - 2025-12-16
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add blob dir size to storage info ([#7605](https://github.com/chatmail/core/pull/7605)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Use `turn.delta.chat` as fallback TURN server ([#7382](https://github.com/chatmail/core/pull/7382)).
|
||||
- Add ip addresses of known public chatmail relays from https://chatmail.at/relays to DNS cache ([#7607](https://github.com/chatmail/core/pull/7607)).
|
||||
- Improve error messages on adding relays.
|
||||
- Add transport addresses to IMAP URLs in message info.
|
||||
- `lookup_host_with_cache()`: Don't return empty address list ([#7596](https://github.com/chatmail/core/pull/7596)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- `get_chat_msgs_ex()`: Don't match on "S=" (Cmd) in param payload.
|
||||
- Remove `SecurejoinWait` info message when received Alice's key ([#7585](https://github.com/chatmail/core/pull/7585)).
|
||||
- Do not set normalized name for existing chats and contacts in a migration.
|
||||
- Remove now redundant "used_account_settings" and "entered_account_settings" from `Context.get_info()` ([#7587](https://github.com/chatmail/core/pull/7587)).
|
||||
- Don't use fallback servers if got TURN servers from IMAP METADATA.
|
||||
- Use fallback ICE servers if server can't IMAP METADATA ([#7382](https://github.com/chatmail/core/pull/7382)).
|
||||
- Add explicit limit for adding relays (5 at the moment) ([#7611](https://github.com/chatmail/core/pull/7611)).
|
||||
- Take `transport_id` into account when using `imap` table.
|
||||
|
||||
### CI
|
||||
|
||||
- Update Rust to 1.92.0.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Apply Rust 1.92.0 clippy suggestions.
|
||||
|
||||
### Other
|
||||
|
||||
- Log entered login params and actual used params on configuration failure ([#7610](https://github.com/chatmail/core/pull/7610)).
|
||||
|
||||
## [2.34.0] - 2025-12-11
|
||||
|
||||
### API-Changes
|
||||
@@ -7411,3 +7706,10 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[2.32.0]: https://github.com/chatmail/core/compare/v2.31.0..v2.32.0
|
||||
[2.33.0]: https://github.com/chatmail/core/compare/v2.32.0..v2.33.0
|
||||
[2.34.0]: https://github.com/chatmail/core/compare/v2.33.0..v2.34.0
|
||||
[2.35.0]: https://github.com/chatmail/core/compare/v2.34.0..v2.35.0
|
||||
[2.36.0]: https://github.com/chatmail/core/compare/v2.35.0..v2.36.0
|
||||
[2.37.0]: https://github.com/chatmail/core/compare/v2.36.0..v2.37.0
|
||||
[2.38.0]: https://github.com/chatmail/core/compare/v2.37.0..v2.38.0
|
||||
[2.39.0]: https://github.com/chatmail/core/compare/v2.38.0..v2.39.0
|
||||
[2.40.0]: https://github.com/chatmail/core/compare/v2.39.0..v2.40.0
|
||||
[2.41.0]: https://github.com/chatmail/core/compare/v2.40.0..v2.41.0
|
||||
|
||||
470
Cargo.lock
generated
470
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "2.34.0"
|
||||
version = "2.41.0"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.88"
|
||||
@@ -111,11 +111,12 @@ toml = "0.9"
|
||||
tracing = "0.1.41"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
walkdir = "2.5.0"
|
||||
webpki-roots = "0.26.8"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
criterion = { version = "0.7.0", features = ["async_tokio"] }
|
||||
criterion = { version = "0.8.1", features = ["async_tokio"] }
|
||||
futures-lite = { workspace = true }
|
||||
log = { workspace = true }
|
||||
nu-ansi-term = { workspace = true }
|
||||
@@ -181,7 +182,7 @@ harness = false
|
||||
anyhow = "1"
|
||||
async-channel = "2.5.0"
|
||||
base64 = "0.22"
|
||||
chrono = { version = "0.4.42", default-features = false }
|
||||
chrono = { version = "0.4.43", default-features = false }
|
||||
deltachat-contact-tools = { path = "deltachat-contact-tools" }
|
||||
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
|
||||
deltachat = { path = ".", default-features = false }
|
||||
@@ -198,10 +199,10 @@ rusqlite = "0.37"
|
||||
sanitize-filename = "0.6"
|
||||
serde = "1.0"
|
||||
serde_json = "1"
|
||||
tempfile = "3.23.0"
|
||||
tempfile = "3.24.0"
|
||||
thiserror = "2"
|
||||
tokio = "1"
|
||||
tokio-util = "0.7.17"
|
||||
tokio-util = "0.7.18"
|
||||
tracing-subscriber = "0.3"
|
||||
yerpc = "0.6.4"
|
||||
|
||||
|
||||
17
RELEASE.md
17
RELEASE.md
@@ -1,4 +1,4 @@
|
||||
# Releasing a new version of DeltaChat core
|
||||
# Releasing a new version of chatmail core
|
||||
|
||||
For example, to release version 1.116.0 of the core, do the following steps.
|
||||
|
||||
@@ -14,8 +14,17 @@ For example, to release version 1.116.0 of the core, do the following steps.
|
||||
5. Commit the changes as `chore(release): prepare for 1.116.0`.
|
||||
Optionally, use a separate branch like `prep-1.116.0` for this commit and open a PR for review.
|
||||
|
||||
6. Tag the release: `git tag --annotate v1.116.0`.
|
||||
6. Push the commit to the `main` branch.
|
||||
|
||||
7. Push the release tag: `git push origin v1.116.0`.
|
||||
7. Once the commit is on the `main` branch and passed CI, tag the release: `git tag --annotate v1.116.0`.
|
||||
|
||||
8. Create a GitHub release: `gh release create v1.116.0 --notes ''`.
|
||||
8. Push the release tag: `git push origin v1.116.0`.
|
||||
|
||||
9. Create a GitHub release: `gh release create v1.116.0 --notes ''`.
|
||||
|
||||
## Dealing with failed releases
|
||||
|
||||
Once you make a GitHub release,
|
||||
CI will try to build and publish [PyPI](https://pypi.org/) and [npm](https://www.npmjs.com/) packages.
|
||||
If this fails for some reason, do not modify the failed tag, do not delete it and do not force-push to the `main` branch.
|
||||
Fix the build process and tag a new release instead.
|
||||
|
||||
2
STYLE.md
2
STYLE.md
@@ -21,7 +21,7 @@ text TEXT DEFAULT '' NOT NULL -- message text
|
||||
```
|
||||
|
||||
Do not use macros like [`concat!`](https://doc.rust-lang.org/std/macro.concat.html)
|
||||
or [`indoc!](https://docs.rs/indoc).
|
||||
or [`indoc!`](https://docs.rs/indoc).
|
||||
Do not escape newlines like this:
|
||||
```
|
||||
sql.execute(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.34.0"
|
||||
version = "2.41.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -22,6 +22,7 @@ typedef struct _dc_lot dc_lot_t;
|
||||
typedef struct _dc_provider dc_provider_t;
|
||||
typedef struct _dc_event dc_event_t;
|
||||
typedef struct _dc_event_emitter dc_event_emitter_t;
|
||||
typedef struct _dc_event_channel dc_event_channel_t;
|
||||
typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_t;
|
||||
typedef struct _dc_backup_provider dc_backup_provider_t;
|
||||
|
||||
@@ -429,16 +430,13 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* 1=send a copy of outgoing messages to self (default).
|
||||
* Sending messages to self is needed for a proper multi-account setup,
|
||||
* however, on the other hand, may lead to unwanted notifications in non-delta clients.
|
||||
* - `sentbox_watch`= 1=watch `Sent`-folder for changes,
|
||||
* 0=do not watch the `Sent`-folder (default).
|
||||
* - `mvbox_move` = 1=detect chat messages,
|
||||
* move them to the `DeltaChat` folder,
|
||||
* and watch the `DeltaChat` folder for updates (default),
|
||||
* 0=do not move chat-messages
|
||||
* - `only_fetch_mvbox` = 1=Do not fetch messages from folders other than the
|
||||
* `DeltaChat` folder. Messages will still be fetched from the
|
||||
* spam folder and `sendbox_watch` will also still be respected
|
||||
* if enabled.
|
||||
* spam folder.
|
||||
* 0=watch all folders normally (default)
|
||||
* - `show_emails` = DC_SHOW_EMAILS_OFF (0)=
|
||||
* show direct replies to chats only,
|
||||
@@ -488,12 +486,15 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* 0=use IMAP IDLE if the server supports it.
|
||||
* This is a developer option used for testing polling used as an IDLE fallback.
|
||||
* - `download_limit` = Messages up to this number of bytes are downloaded automatically.
|
||||
* For larger messages, only the header is downloaded and a placeholder is shown.
|
||||
* These messages can be downloaded fully using dc_download_full_msg() later.
|
||||
* The limit is compared against raw message sizes, including headers.
|
||||
* The actually used limit may be corrected
|
||||
* to not mess up with non-delivery-reports or read-receipts.
|
||||
* 0=no limit (default).
|
||||
* For messages with large attachments, two messages are sent:
|
||||
* a Pre-Message containing metadata and text and a Post-Message additionally
|
||||
* containing the attachment. NB: Some "extra" metadata like avatars and gossiped
|
||||
* encryption keys is stripped from post-messages to save traffic.
|
||||
* Pre-Messages are shown as placeholder messages. They can be downloaded fully
|
||||
* using dc_download_full_msg() later. Post-Messages are automatically
|
||||
* downloaded if they are smaller than the download_limit. Other messages are
|
||||
* always auto-downloaded.
|
||||
* 0 = no limit (default).
|
||||
* Changes affect future messages only.
|
||||
* - `protect_autocrypt` = Enable Header Protection for Autocrypt header.
|
||||
* This is an experimental option not compatible to other MUAs
|
||||
@@ -519,6 +520,10 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* - `webxdc_realtime_enabled` = Whether the realtime APIs should be enabled.
|
||||
* 0 = WebXDC realtime API is disabled and behaves as noop.
|
||||
* 1 = WebXDC realtime API is enabled (default).
|
||||
* - `who_can_call_me` = Who can cause call notifications.
|
||||
* 0 = Everybody (except explicitly blocked contacts),
|
||||
* 1 = Contacts (default, does not include contact requests),
|
||||
* 2 = Nobody (calls never result in a notification).
|
||||
*
|
||||
* If you want to retrieve a value, use dc_get_config().
|
||||
*
|
||||
@@ -575,11 +580,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 or DC_QR_LOGIN.
|
||||
* Before this function is called, dc_check_qr() should be used to get the QR code type.
|
||||
*
|
||||
* 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.
|
||||
* DC_QR_ACCOUNT and DC_QR_LOGIN QR codes configure the context, but I/O mustn't be started for such
|
||||
* QR codes.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
@@ -890,7 +894,7 @@ int dc_preconfigure_keypair (dc_context_t* context, const cha
|
||||
* the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are _any_ archived
|
||||
* chats
|
||||
* - the flag DC_GCL_FOR_FORWARDING sorts "Saved messages" to the top of the chatlist
|
||||
* and hides the "Device chat" and contact requests.
|
||||
* and hides the "Device chat", contact requests and incoming broadcasts.
|
||||
* typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
|
||||
* to also hide the archive link.
|
||||
* - if the flag DC_GCL_NO_SPECIALS is set, archive link is not added
|
||||
@@ -1238,9 +1242,12 @@ uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t c
|
||||
* This needs to be a one-to-one chat.
|
||||
* @param place_call_info any data that other devices receive
|
||||
* in #DC_EVENT_INCOMING_CALL.
|
||||
* @param has_video Whether the call has video initially.
|
||||
* This allows the recipient's client to adjust incoming call UX.
|
||||
* A call can be upgraded to include video later.
|
||||
* @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);
|
||||
uint32_t dc_place_outgoing_call (dc_context_t* context, uint32_t chat_id, const char* place_call_info, int has_video);
|
||||
|
||||
|
||||
/**
|
||||
@@ -1559,7 +1566,7 @@ dc_array_t* dc_wait_next_msgs (dc_context_t* context);
|
||||
* Mark all messages in a chat as _noticed_.
|
||||
* _Noticed_ messages are no longer _fresh_ and do not count as being unseen
|
||||
* but are still waiting for being marked as "seen" using dc_markseen_msgs()
|
||||
* (IMAP/MDNs is not done for noticed messages).
|
||||
* (read receipts aren't sent for noticed messages).
|
||||
*
|
||||
* Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED.
|
||||
* See also dc_markseen_msgs().
|
||||
@@ -1611,10 +1618,10 @@ void dc_set_chat_visibility (dc_context_t* context, uint32_t ch
|
||||
*
|
||||
* Messages are deleted from the device and the chat database entry is deleted.
|
||||
* After that, the event #DC_EVENT_MSGS_CHANGED is posted.
|
||||
* Messages are deleted from the server in background.
|
||||
*
|
||||
* Things that are _not_ done implicitly:
|
||||
*
|
||||
* - Messages are **not deleted from the server**.
|
||||
* - The chat or the contact is **not blocked**, so new messages from the user/the group may appear
|
||||
* and the user may create the chat again.
|
||||
* - **Groups are not left** - this would
|
||||
@@ -2216,10 +2223,6 @@ uint32_t dc_lookup_contact_id_by_addr (dc_context_t* context, const char*
|
||||
uint32_t dc_create_contact (dc_context_t* context, const char* name, const char* addr);
|
||||
|
||||
|
||||
|
||||
// Deprecated 2025-05-20, setting this flag is a no-op.
|
||||
#define DC_GCL_DEPRECATED_VERIFIED_ONLY 0x01
|
||||
|
||||
#define DC_GCL_ADD_SELF 0x02
|
||||
#define DC_GCL_ADDRESS 0x04
|
||||
|
||||
@@ -2292,17 +2295,6 @@ dc_array_t* dc_import_vcard (dc_context_t* context, const char*
|
||||
dc_array_t* dc_get_contacts (dc_context_t* context, uint32_t flags, const char* query);
|
||||
|
||||
|
||||
/**
|
||||
* Get the number of blocked contacts.
|
||||
*
|
||||
* @deprecated Deprecated 2021-02-22, use dc_array_get_cnt() on dc_get_blocked_contacts() instead.
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @return The number of blocked contacts.
|
||||
*/
|
||||
int dc_get_blocked_cnt (dc_context_t* context);
|
||||
|
||||
|
||||
/**
|
||||
* Get blocked contacts.
|
||||
*
|
||||
@@ -2575,7 +2567,6 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
#define DC_QR_FPR_MISMATCH 220 // id=contact
|
||||
#define DC_QR_FPR_WITHOUT_ADDR 230 // test1=formatted fingerprint
|
||||
#define DC_QR_ACCOUNT 250 // text1=domain
|
||||
#define DC_QR_BACKUP 251 // deprecated
|
||||
#define DC_QR_BACKUP2 252
|
||||
#define DC_QR_BACKUP_TOO_NEW 255
|
||||
#define DC_QR_PROXY 271 // text1=address (e.g. "127.0.0.1:9050")
|
||||
@@ -3092,7 +3083,7 @@ int dc_receive_backup (dc_context_t* context, const char* qr);
|
||||
|
||||
/**
|
||||
* Create a new account manager.
|
||||
* The account manager takes an directory
|
||||
* The account manager takes a directory
|
||||
* where all context-databases are placed in.
|
||||
* To add a context to the account manager,
|
||||
* use dc_accounts_add_account() or dc_accounts_migrate_account().
|
||||
@@ -3114,6 +3105,35 @@ int dc_receive_backup (dc_context_t* context, const char* qr);
|
||||
*/
|
||||
dc_accounts_t* dc_accounts_new (const char* dir, int writable);
|
||||
|
||||
/**
|
||||
* Create a new account manager with an existing events channel,
|
||||
* which allows you to see events emitted during startup.
|
||||
*
|
||||
* The account manager takes a directory
|
||||
* where all context-databases are placed in.
|
||||
* To add a context to the account manager,
|
||||
* use dc_accounts_add_account() or dc_accounts_migrate_account().
|
||||
* All account information are persisted.
|
||||
* To remove a context from the account manager,
|
||||
* use dc_accounts_remove_account().
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param dir The directory to create the context-databases in.
|
||||
* If the directory does not exist,
|
||||
* dc_accounts_new_with_event_channel() will try to create it.
|
||||
* @param writable Whether the returned account manager is writable, i.e. calling these functions on
|
||||
* it is possible: dc_accounts_add_account(), dc_accounts_add_closed_account(),
|
||||
* dc_accounts_migrate_account(), dc_accounts_remove_account(), dc_accounts_select_account().
|
||||
* @param dc_event_channel_t Events Channel to be used for this accounts manager,
|
||||
* create one with dc_event_channel_new().
|
||||
* This channel is consumed by this method and can not be used again afterwards,
|
||||
* so be sure to call `dc_event_channel_get_event_emitter` before.
|
||||
* @return An account manager object.
|
||||
* The object must be passed to the other account manager functions
|
||||
* and must be freed using dc_accounts_unref() after usage.
|
||||
* On errors, NULL is returned.
|
||||
*/
|
||||
dc_accounts_t* dc_accounts_new_with_event_channel(const char* dir, int writable, dc_event_channel_t* events_channel);
|
||||
|
||||
/**
|
||||
* Free an account manager object.
|
||||
@@ -3354,8 +3374,12 @@ void dc_accounts_set_push_device_token (dc_accounts_t* accounts, const
|
||||
* Must be freed using dc_event_emitter_unref() after usage.
|
||||
*
|
||||
* Note: Use only one event emitter per account manager.
|
||||
* Having more than one event emitter running at the same time on the same account manager
|
||||
* will result in events randomly delivered to the one or to the other.
|
||||
* The result of having multiple event emitters is unspecified.
|
||||
* Currently events are broadcasted to all existing event emitters,
|
||||
* but previous versions delivered events to only one event emitter
|
||||
* and this behavior may change again in the future.
|
||||
* Events emitted before creation of event emitter
|
||||
* are not available to event emitter.
|
||||
*/
|
||||
dc_event_emitter_t* dc_accounts_get_event_emitter (dc_accounts_t* accounts);
|
||||
|
||||
@@ -3732,30 +3756,7 @@ uint32_t dc_chat_get_id (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Get chat type as one of the @ref DC_CHAT_TYPE constants:
|
||||
*
|
||||
* - @ref DC_CHAT_TYPE_SINGLE - a normal chat is a chat with a single contact,
|
||||
* chats_contacts contains one record for the user. DC_CONTACT_ID_SELF
|
||||
* (see dc_contact_t::id) is added _only_ for a self talk.
|
||||
* These chats are created by dc_create_chat_by_contact_id().
|
||||
*
|
||||
* - @ref DC_CHAT_TYPE_GROUP - a group chat, chats_contacts contain all group
|
||||
* members, incl. DC_CONTACT_ID_SELF.
|
||||
* Groups are created by dc_create_group_chat().
|
||||
*
|
||||
* - @ref DC_CHAT_TYPE_MAILINGLIST - a mailing list, this is similar to groups,
|
||||
* however, the member list cannot be retrieved completely
|
||||
* and cannot be changed using this api.
|
||||
* Mailing lists are created as needed by incoming messages
|
||||
* and usually require some special server;
|
||||
* they cannot be created by a function call as the other chat types.
|
||||
* Moreover, for now, mailing lists are read-only.
|
||||
*
|
||||
* - @ref DC_CHAT_TYPE_BROADCAST - a broadcast list,
|
||||
* the recipients will get messages in a one-to-one chats and
|
||||
* the sender will get answers in a one-to-one as well.
|
||||
* chats_contacts contain all recipients but DC_CONTACT_ID_SELF.
|
||||
* Broadcasts are created by dc_create_broadcast_list().
|
||||
* Get chat type as one of the @ref DC_CHAT_TYPE constants.
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
* @param chat The chat object.
|
||||
@@ -4311,6 +4312,7 @@ char* dc_msg_get_webxdc_info (const dc_msg_t* msg);
|
||||
/**
|
||||
* Get the size of the file. Returns the size of the file associated with a
|
||||
* message, if applicable.
|
||||
* If message is a pre-message, then this returns the size of the file to be downloaded.
|
||||
*
|
||||
* Typically, this is used to show the size of document files, e.g. a PDF.
|
||||
*
|
||||
@@ -4660,7 +4662,6 @@ 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 // deprecated 2025-07
|
||||
#define DC_INFO_INVALID_UNENCRYPTED_MAIL 13
|
||||
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
|
||||
#define DC_INFO_CHAT_E2EE 50
|
||||
@@ -5328,8 +5329,8 @@ int dc_contact_is_key_contact (dc_contact_t* contact);
|
||||
*
|
||||
* - 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().
|
||||
* with the name of the contact
|
||||
* formatted by dc_contact_get_name().
|
||||
* Prefix the text by a green checkmark.
|
||||
*
|
||||
* - If dc_contact_get_verifier_id() == 0 and dc_contact_is_verified() != 0,
|
||||
@@ -5760,17 +5761,32 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
#define DC_CHAT_TYPE_UNDEFINED 0
|
||||
|
||||
/**
|
||||
* A one-to-one chat with a single contact. See dc_chat_get_type() for details.
|
||||
* A one-to-one chat with a single contact.
|
||||
*
|
||||
* dc_get_chat_contacts() contains one record for the user.
|
||||
* DC_CONTACT_ID_SELF is added _only_ for a self talk.
|
||||
* These chats are created by dc_create_chat_by_contact_id().
|
||||
*/
|
||||
#define DC_CHAT_TYPE_SINGLE 100
|
||||
|
||||
/**
|
||||
* A group chat. See dc_chat_get_type() for details.
|
||||
* A group chat.
|
||||
*
|
||||
* dc_get_chat_contacts() contain all group members,
|
||||
* including DC_CONTACT_ID_SELF.
|
||||
* Groups are created by dc_create_group_chat().
|
||||
*/
|
||||
#define DC_CHAT_TYPE_GROUP 120
|
||||
|
||||
/**
|
||||
* A mailing list. See dc_chat_get_type() for details.
|
||||
* A mailing list.
|
||||
*
|
||||
* This is similar to groups,
|
||||
* however, the member list cannot be retrieved completely
|
||||
* and cannot be changed using an API from this library.
|
||||
* Mailing lists are created as needed by incoming messages
|
||||
* and usually require some special server;
|
||||
* they cannot be created by a function call as the other chat types.
|
||||
*/
|
||||
#define DC_CHAT_TYPE_MAILINGLIST 140
|
||||
|
||||
@@ -5999,6 +6015,62 @@ char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);
|
||||
*/
|
||||
char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *input);
|
||||
|
||||
/**
|
||||
* @class dc_event_channel_t
|
||||
*
|
||||
* Opaque object that is used to create an event emitter which can be used log events during startup of an accounts manger.
|
||||
* Only used for dc_accounts_new_with_event_channel().
|
||||
* To use it:
|
||||
* 1. create an events channel with `dc_event_channel_new()`.
|
||||
* 2. get an event emitter for it with `dc_event_channel_get_event_emitter()`.
|
||||
* 3. use it to create your account manager with `dc_accounts_new_with_event_channel()`, which consumes the channel.
|
||||
* 4. free the empty channel wrapper object with `dc_event_channel_unref()`.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a new event channel.
|
||||
*
|
||||
* @memberof dc_event_channel_t
|
||||
* @return An event channel wrapper object (dc_event_channel_t).
|
||||
*/
|
||||
dc_event_channel_t* dc_event_channel_new();
|
||||
|
||||
/**
|
||||
* Release/free the events channel structure.
|
||||
* This function releases the memory of the `dc_event_channel_t` structure.
|
||||
*
|
||||
* you can call it after calling dc_accounts_new_with_event_channel,
|
||||
* which took the events channel out of it already, so this just frees the underlying option.
|
||||
*
|
||||
* @memberof dc_event_channel_t
|
||||
*/
|
||||
void dc_event_channel_unref(dc_event_channel_t* event_channel);
|
||||
|
||||
/**
|
||||
* Create the event emitter that is used to receive events.
|
||||
*
|
||||
* The library will emit various @ref DC_EVENT events, such as "new message", "message read" etc.
|
||||
* To get these events, you have to create an event emitter using this function
|
||||
* and call dc_get_next_event() on the emitter.
|
||||
*
|
||||
* This is similar to dc_get_event_emitter(), which, however,
|
||||
* must not be called for accounts handled by the account manager.
|
||||
*
|
||||
* @memberof dc_event_channel_t
|
||||
* @param The event channel.
|
||||
* @return Returns the event emitter, NULL on errors.
|
||||
* Must be freed using dc_event_emitter_unref() after usage.
|
||||
*
|
||||
* Note: Use only one event emitter per account manager / event channel.
|
||||
* The result of having multiple event emitters is unspecified.
|
||||
* Currently events are broadcasted to all existing event emitters,
|
||||
* but previous versions delivered events to only one event emitter
|
||||
* and this behavior may change again in the future.
|
||||
* Events emitted before creation of event emitter
|
||||
* are not available to event emitter.
|
||||
*/
|
||||
dc_event_emitter_t* dc_event_channel_get_event_emitter(dc_event_channel_t* event_channel);
|
||||
|
||||
/**
|
||||
* @class dc_event_emitter_t
|
||||
*
|
||||
@@ -6702,6 +6774,16 @@ void dc_event_unref(dc_event_t* event);
|
||||
*/
|
||||
#define DC_EVENT_CALL_ENDED 2580
|
||||
|
||||
/**
|
||||
* Transport relay added/deleted or default has changed.
|
||||
* UI should update the list.
|
||||
*
|
||||
* The event is emitted when the transports are modified on another device
|
||||
* using the JSON-RPC calls `add_or_update_transport`, `add_transport_from_qr`, `delete_transport`
|
||||
* or `set_config(configured_addr)`.
|
||||
*/
|
||||
#define DC_EVENT_TRANSPORTS_MODIFIED 2600
|
||||
|
||||
|
||||
/**
|
||||
* @}
|
||||
@@ -6926,61 +7008,16 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used in summaries.
|
||||
#define DC_STR_FILE 12
|
||||
|
||||
/// "Group name changed from %1$s to %2$s."
|
||||
///
|
||||
/// Used in status messages for group name changes.
|
||||
/// - %1$s will be replaced by the old group name
|
||||
/// - %2$s will be replaced by the new group name
|
||||
///
|
||||
/// @deprecated 2022-09-10
|
||||
#define DC_STR_MSGGRPNAME 15
|
||||
|
||||
/// "Group image changed."
|
||||
///
|
||||
/// Used in status messages for group images changes.
|
||||
///
|
||||
/// @deprecated 2022-09-10
|
||||
#define DC_STR_MSGGRPIMGCHANGED 16
|
||||
|
||||
/// "Member %1$s added."
|
||||
///
|
||||
/// Used in status messages for added members.
|
||||
/// - %1$s will be replaced by the name of the added member
|
||||
///
|
||||
/// @deprecated 2022-09-10
|
||||
#define DC_STR_MSGADDMEMBER 17
|
||||
|
||||
/// "Member %1$s removed."
|
||||
///
|
||||
/// Used in status messages for removed members.
|
||||
/// - %1$s will be replaced by the name of the removed member
|
||||
///
|
||||
/// @deprecated 2022-09-10
|
||||
#define DC_STR_MSGDELMEMBER 18
|
||||
|
||||
/// "Group left."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// @deprecated 2022-09-10
|
||||
#define DC_STR_MSGGROUPLEFT 19
|
||||
|
||||
/// "GIF"
|
||||
///
|
||||
/// Used in summaries.
|
||||
#define DC_STR_GIF 23
|
||||
|
||||
/// @deprecated 2025-07, this string is no longer needed.
|
||||
#define DC_STR_ENCRYPTEDMSG 24
|
||||
|
||||
/// "End-to-end encryption available."
|
||||
///
|
||||
/// Used to build the string returned by dc_get_contact_encrinfo().
|
||||
/// @deprecated 2026-01-23
|
||||
#define DC_STR_E2E_AVAILABLE 25
|
||||
|
||||
/// @deprecated Deprecated 2021-02-07, this string is no longer needed.
|
||||
#define DC_STR_ENCR_TRANSP 27
|
||||
|
||||
/// "No encryption."
|
||||
///
|
||||
/// Used to build the string returned by dc_get_contact_encrinfo().
|
||||
@@ -6991,90 +7028,23 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used to build the string returned by dc_get_contact_encrinfo().
|
||||
#define DC_STR_FINGERPRINTS 30
|
||||
|
||||
/// "Message opened"
|
||||
///
|
||||
/// Used in subjects of outgoing read receipts.
|
||||
///
|
||||
/// @deprecated Deprecated 2024-07-26
|
||||
#define DC_STR_READRCPT 31
|
||||
|
||||
/// "The message '%1$s' you sent was displayed on the screen of the recipient."
|
||||
///
|
||||
/// Used as message text of outgoing read receipts.
|
||||
/// - %1$s will be replaced by the subject of the displayed message
|
||||
///
|
||||
/// @deprecated Deprecated 2024-06-23
|
||||
#define DC_STR_READRCPT_MAILBODY 32
|
||||
|
||||
/// @deprecated Deprecated, this string is no longer needed.
|
||||
#define DC_STR_MSGGRPIMGDELETED 33
|
||||
|
||||
/// "End-to-end encryption preferred."
|
||||
///
|
||||
/// Used to build the string returned by dc_get_contact_encrinfo().
|
||||
/// @deprecated 2025-06-05
|
||||
#define DC_STR_E2E_PREFERRED 34
|
||||
|
||||
/// "%1$s verified"
|
||||
///
|
||||
/// Used in status messages.
|
||||
/// - %1$s will be replaced by the name of the verified contact
|
||||
#define DC_STR_CONTACT_VERIFIED 35
|
||||
|
||||
/// "Cannot establish guaranteed end-to-end encryption with %1$s."
|
||||
///
|
||||
/// Used in status messages.
|
||||
/// - %1$s will be replaced by the name of the contact that cannot be verified
|
||||
/// @deprecated 2025-06-05
|
||||
#define DC_STR_CONTACT_NOT_VERIFIED 36
|
||||
|
||||
/// "Changed setup for %1$s."
|
||||
///
|
||||
/// Used in status messages.
|
||||
/// - %1$s will be replaced by the name of the contact with the changed setup
|
||||
/// @deprecated 2025-06-05
|
||||
#define DC_STR_CONTACT_SETUP_CHANGED 37
|
||||
|
||||
/// "Archived chats"
|
||||
///
|
||||
/// Used as the name for the corresponding chatlist entry.
|
||||
#define DC_STR_ARCHIVEDCHATS 40
|
||||
|
||||
/// "Autocrypt Setup Message"
|
||||
///
|
||||
/// @deprecated 2025-04
|
||||
#define DC_STR_AC_SETUP_MSG_SUBJECT 42
|
||||
|
||||
/// "This is the Autocrypt Setup Message, open it in a compatible client to use your setup"
|
||||
///
|
||||
/// @deprecated 2025-04
|
||||
#define DC_STR_AC_SETUP_MSG_BODY 43
|
||||
|
||||
/// "Cannot login as %1$s."
|
||||
///
|
||||
/// Used in error strings.
|
||||
/// - %1$s will be replaced by the failing login name
|
||||
#define DC_STR_CANNOT_LOGIN 60
|
||||
|
||||
/// "%1$s by %2$s"
|
||||
///
|
||||
/// Used to concretize actions,
|
||||
/// - %1$s will be replaced by an action
|
||||
/// as #DC_STR_MSGADDMEMBER or #DC_STR_MSGGRPIMGCHANGED (full-stop removed, if any)
|
||||
/// - %2$s will be replaced by the name of the user taking that action
|
||||
///
|
||||
/// @deprecated 2022-09-10
|
||||
#define DC_STR_MSGACTIONBYUSER 62
|
||||
|
||||
/// "%1$s by me"
|
||||
///
|
||||
/// Used to concretize actions.
|
||||
/// - %1$s will be replaced by an action
|
||||
/// as #DC_STR_MSGADDMEMBER or #DC_STR_MSGGRPIMGCHANGED (full-stop removed, if any)
|
||||
///
|
||||
/// @deprecated 2022-09-10
|
||||
#define DC_STR_MSGACTIONBYME 63
|
||||
|
||||
/// "Location streaming enabled."
|
||||
///
|
||||
/// Used in status messages.
|
||||
@@ -7115,13 +7085,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used as message text for the message added to the device chat after successful login.
|
||||
#define DC_STR_WELCOME_MESSAGE 71
|
||||
|
||||
/// "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"
|
||||
///
|
||||
/// Used in subjects of outgoing messages in one-to-one chats.
|
||||
@@ -7135,53 +7098,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// - %1$s will be replaced by the name of the contact the message cannot be sent to
|
||||
#define DC_STR_FAILED_SENDING_TO 74
|
||||
|
||||
/// "Message deletion timer is disabled."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// @deprecated 2022-09-10
|
||||
#define DC_STR_EPHEMERAL_DISABLED 75
|
||||
|
||||
/// "Message deletion timer is set to %1$s s."
|
||||
///
|
||||
/// Used in status messages when the other constants
|
||||
/// (#DC_STR_EPHEMERAL_MINUTE, #DC_STR_EPHEMERAL_HOUR and so on) do not match the timer.
|
||||
/// - %1$s will be replaced by the number of seconds the timer is set to
|
||||
///
|
||||
/// @deprecated 2022-09-10
|
||||
#define DC_STR_EPHEMERAL_SECONDS 76
|
||||
|
||||
/// "Message deletion timer is set to 1 minute."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// @deprecated 2022-09-10
|
||||
#define DC_STR_EPHEMERAL_MINUTE 77
|
||||
|
||||
/// "Message deletion timer is set to 1 hour."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// @deprecated 2022-09-10
|
||||
#define DC_STR_EPHEMERAL_HOUR 78
|
||||
|
||||
/// "Message deletion timer is set to 1 day."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// @deprecated 2022-09-10
|
||||
#define DC_STR_EPHEMERAL_DAY 79
|
||||
|
||||
/// "Message deletion timer is set to 1 week."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// @deprecated 2022-09-10
|
||||
#define DC_STR_EPHEMERAL_WEEK 80
|
||||
|
||||
/// @deprecated Deprecated 2021-01-30, DC_STR_EPHEMERAL_WEEKS is used instead.
|
||||
#define DC_STR_EPHEMERAL_FOUR_WEEKS 81
|
||||
|
||||
/// "Error: %1$s"
|
||||
///
|
||||
/// Used in error strings.
|
||||
@@ -7215,42 +7131,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used as device message text.
|
||||
#define DC_STR_SELF_DELETED_MSG_BODY 91
|
||||
|
||||
/// "Message deletion timer is set to %1$s minutes."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// `%1$s` will be replaced by the number of minutes (always >1) the timer is set to.
|
||||
///
|
||||
/// @deprecated Replaced by DC_STR_MSG_YOU_EPHEMERAL_TIMER_MINUTES and DC_STR_MSG_EPHEMERAL_TIMER_MINUTES_BY.
|
||||
#define DC_STR_EPHEMERAL_MINUTES 93
|
||||
|
||||
/// "Message deletion timer is set to %1$s hours."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// `%1$s` will be replaced by the number of hours (always >1) the timer is set to.
|
||||
///
|
||||
/// @deprecated Replaced by DC_STR_MSG_YOU_EPHEMERAL_TIMER_HOURS and DC_STR_MSG_EPHEMERAL_TIMER_HOURS_BY.
|
||||
#define DC_STR_EPHEMERAL_HOURS 94
|
||||
|
||||
/// "Message deletion timer is set to %1$s days."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// `%1$s` will be replaced by the number of days (always >1) the timer is set to.
|
||||
///
|
||||
/// @deprecated Replaced by DC_STR_MSG_YOU_EPHEMERAL_TIMER_DAYS and DC_STR_MSG_EPHEMERAL_TIMER_DAYS_BY.
|
||||
#define DC_STR_EPHEMERAL_DAYS 95
|
||||
|
||||
/// "Message deletion timer is set to %1$s weeks."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// `%1$s` will be replaced by the number of weeks (always >1) the timer is set to.
|
||||
///
|
||||
/// @deprecated Replaced by DC_STR_MSG_YOU_EPHEMERAL_TIMER_WEEKS and DC_STR_MSG_EPHEMERAL_TIMER_WEEKS_BY.
|
||||
#define DC_STR_EPHEMERAL_WEEKS 96
|
||||
|
||||
/// "Forwarded"
|
||||
///
|
||||
/// Used in message summary text for notifications and chatlist.
|
||||
@@ -7263,22 +7143,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// `%1$s` will be replaced by the percentage used
|
||||
#define DC_STR_QUOTA_EXCEEDING_MSG_BODY 98
|
||||
|
||||
/// "%1$s message"
|
||||
///
|
||||
/// Used as the message body when a message
|
||||
/// was not yet downloaded completely
|
||||
/// (dc_msg_get_download_state() is e.g. @ref DC_DOWNLOAD_AVAILABLE).
|
||||
///
|
||||
/// `%1$s` will be replaced by human-readable size (e.g. "1.2 MiB").
|
||||
#define DC_STR_PARTIAL_DOWNLOAD_MSG_BODY 99
|
||||
|
||||
/// "Download maximum available until %1$s"
|
||||
///
|
||||
/// Appended after some separator to @ref DC_STR_PARTIAL_DOWNLOAD_MSG_BODY.
|
||||
///
|
||||
/// `%1$s` will be replaced by human-readable date and time.
|
||||
#define DC_STR_DOWNLOAD_AVAILABILITY 100
|
||||
|
||||
/// "Multi Device Synchronization"
|
||||
///
|
||||
/// Used in subjects of outgoing sync messages.
|
||||
@@ -7304,16 +7168,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used as a headline in the connectivity view.
|
||||
#define DC_STR_OUTGOING_MESSAGES 104
|
||||
|
||||
/// "Storage on %1$s"
|
||||
///
|
||||
/// Used as a headline in the connectivity view.
|
||||
///
|
||||
/// `%1$s` will be replaced by the domain of the configured e-mail address.
|
||||
#define DC_STR_STORAGE_ON_DOMAIN 105
|
||||
|
||||
/// @deprecated Deprecated 2022-04-16, this string is no longer needed.
|
||||
#define DC_STR_ONE_MOMENT 106
|
||||
|
||||
/// "Connected"
|
||||
///
|
||||
/// Used as status in the connectivity view.
|
||||
@@ -7372,8 +7226,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// May be followed by the info-messages
|
||||
/// #DC_STR_SECURE_JOIN_REPLIES, #DC_STR_CONTACT_VERIFIED and #DC_STR_MSGADDMEMBER.
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the inviter,
|
||||
/// `%2$s` will be replaced by the name of the inviter.
|
||||
/// `%1$s` and `%2$s` will be replaced by name of the inviter.
|
||||
#define DC_STR_SECURE_JOIN_STARTED 117
|
||||
|
||||
/// "%1$s replied, waiting for being added to the group…"
|
||||
@@ -7390,7 +7243,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
///
|
||||
/// Subtitle for verification qrcode svg image generated by the core.
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the inviter.
|
||||
/// `%1$s` will be replaced by name of the inviter.
|
||||
#define DC_STR_SETUP_CONTACT_QR_DESC 119
|
||||
|
||||
/// "Scan to join %1$s"
|
||||
@@ -7405,12 +7258,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used as status in the connectivity view.
|
||||
#define DC_STR_NOT_CONNECTED 121
|
||||
|
||||
/// "%1$s changed their address from %2$s to %3$s"
|
||||
///
|
||||
/// Used as an info message to chats with contacts that changed their address.
|
||||
/// @deprecated 2025-06-05
|
||||
#define DC_STR_AEAP_ADDR_CHANGED 122
|
||||
|
||||
/// "You changed group name from \"%1$s\" to \"%2$s\"."
|
||||
///
|
||||
/// `%1$s` will be replaced by the old group name.
|
||||
@@ -7421,7 +7268,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
///
|
||||
/// `%1$s` will be replaced by the old group name.
|
||||
/// `%2$s` will be replaced by the new group name.
|
||||
/// `%3$s` will be replaced by name and address of the contact who did the action.
|
||||
/// `%3$s` will be replaced by name of the contact who did the action.
|
||||
#define DC_STR_GROUP_NAME_CHANGED_BY_OTHER 125
|
||||
|
||||
/// "You changed the group image."
|
||||
@@ -7429,7 +7276,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
/// "Group image changed by %1$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact who did the action.
|
||||
/// `%1$s` will be replaced by name of the contact who did the action.
|
||||
#define DC_STR_GROUP_IMAGE_CHANGED_BY_OTHER 127
|
||||
|
||||
/// "You added member %1$s."
|
||||
@@ -7441,23 +7288,23 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
/// "Member %1$s added by %2$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact added to the group.
|
||||
/// `%2$s` will be replaced by name and address of the contact who did the action.
|
||||
/// `%1$s` will be replaced by name of the contact added to the group.
|
||||
/// `%2$s` will be replaced by name of the contact who did the action.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_ADD_MEMBER_BY_OTHER 129
|
||||
|
||||
/// "You removed member %1$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact removed from the group.
|
||||
/// `%1$s` will be replaced by name of the contact removed from the group.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_REMOVE_MEMBER_BY_YOU 130
|
||||
|
||||
/// "Member %1$s removed by %2$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact removed from the group.
|
||||
/// `%2$s` will be replaced by name and address of the contact who did the action.
|
||||
/// `%1$s` will be replaced by name of the contact removed from the group.
|
||||
/// `%2$s` will be replaced by name of the contact who did the action.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_REMOVE_MEMBER_BY_OTHER 131
|
||||
@@ -7469,7 +7316,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
/// "Group left by %1$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact.
|
||||
/// `%1$s` will be replaced by name of the contact.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_GROUP_LEFT_BY_OTHER 133
|
||||
@@ -7481,7 +7328,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
/// "Group image deleted by %1$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact.
|
||||
/// `%1$s` will be replaced by name of the contact.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_GROUP_IMAGE_DELETED_BY_OTHER 135
|
||||
@@ -7493,7 +7340,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
/// "Location streaming enabled by %1$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact.
|
||||
/// `%1$s` will be replaced by name of the contact.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_LOCATION_ENABLED_BY_OTHER 137
|
||||
@@ -7505,7 +7352,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
/// "Message deletion timer is disabled by %1$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact.
|
||||
/// `%1$s` will be replaced by name of the contact.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_EPHEMERAL_TIMER_DISABLED_BY_OTHER 139
|
||||
@@ -7520,22 +7367,11 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// "Message deletion timer is set to %1$s s by %2$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by the number of seconds (always >1) the timer is set to.
|
||||
/// `%2$s` will be replaced by name and address of the contact.
|
||||
/// `%2$s` will be replaced by name of the contact.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_EPHEMERAL_TIMER_SECONDS_BY_OTHER 141
|
||||
|
||||
/// "You set message deletion timer to 1 minute."
|
||||
///
|
||||
/// @deprecated 2025-11-14, this string is no longer needed
|
||||
#define DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_YOU 142
|
||||
|
||||
/// "Message deletion timer is set to 1 minute by %1$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact.
|
||||
/// @deprecated 2025-11-14, this string is no longer needed
|
||||
#define DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_OTHER 143
|
||||
|
||||
/// "You set message deletion timer to 1 hour."
|
||||
///
|
||||
/// Used in status messages.
|
||||
@@ -7543,7 +7379,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
/// "Message deletion timer is set to 1 hour by %1$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact.
|
||||
/// `%1$s` will be replaced by name of the contact.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_OTHER 145
|
||||
@@ -7555,7 +7391,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
/// "Message deletion timer is set to 1 day by %1$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact.
|
||||
/// `%1$s` will be replaced by name of the contact.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_EPHEMERAL_TIMER_1_DAY_BY_OTHER 147
|
||||
@@ -7567,7 +7403,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
/// "Message deletion timer is set to 1 week by %1$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact.
|
||||
/// `%1$s` will be replaced by name of the contact.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_OTHER 149
|
||||
@@ -7584,7 +7420,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// `%1$s` will be replaced by the number of minutes (always >1) the timer is set to.
|
||||
/// `%2$s` will be replaced by name and address of the contact.
|
||||
/// `%2$s` will be replaced by name of the contact.
|
||||
#define DC_STR_EPHEMERAL_TIMER_MINUTES_BY_OTHER 151
|
||||
|
||||
/// "You set message deletion timer to %1$s hours."
|
||||
@@ -7599,7 +7435,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// `%1$s` will be replaced by the number of hours (always >1) the timer is set to.
|
||||
/// `%2$s` will be replaced by name and address of the contact.
|
||||
/// `%2$s` will be replaced by name of the contact.
|
||||
#define DC_STR_EPHEMERAL_TIMER_HOURS_BY_OTHER 153
|
||||
|
||||
/// "You set message deletion timer to %1$s days."
|
||||
@@ -7614,7 +7450,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// `%1$s` will be replaced by the number of days (always >1) the timer is set to.
|
||||
/// `%2$s` will be replaced by name and address of the contact.
|
||||
/// `%2$s` will be replaced by name of the contact.
|
||||
#define DC_STR_EPHEMERAL_TIMER_DAYS_BY_OTHER 155
|
||||
|
||||
/// "You set message deletion timer to %1$s weeks."
|
||||
@@ -7629,7 +7465,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// `%1$s` will be replaced by the number of weeks (always >1) the timer is set to.
|
||||
/// `%2$s` will be replaced by name and address of the contact.
|
||||
/// `%2$s` will be replaced by name of the contact.
|
||||
#define DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER 157
|
||||
|
||||
/// "You set message deletion timer to 1 year."
|
||||
@@ -7639,14 +7475,14 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
/// "Message deletion timer is set to 1 year by %1$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact.
|
||||
/// `%1$s` will be replaced by name of the contact.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_EPHEMERAL_TIMER_1_YEAR_BY_OTHER 159
|
||||
|
||||
/// "Scan to set up second device for %1$s"
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the account.
|
||||
/// `%1$s` will be replaced by name of the account.
|
||||
#define DC_STR_BACKUP_TRANSFER_QR 162
|
||||
|
||||
/// "Account transferred to your second device."
|
||||
@@ -7705,26 +7541,9 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used as info message.
|
||||
#define DC_STR_SECUREJOIN_WAIT 190
|
||||
|
||||
/// "Could not yet establish guaranteed end-to-end encryption, but you may already send a message."
|
||||
///
|
||||
/// @deprecated 2025-03
|
||||
#define DC_STR_SECUREJOIN_WAIT_TIMEOUT 191
|
||||
|
||||
/// "The contact must be online to proceed. This process will continue automatically in background."
|
||||
///
|
||||
/// Used as info message.
|
||||
/// @deprecated 2025-06-05
|
||||
#define DC_STR_SECUREJOIN_TAKES_LONGER 192
|
||||
|
||||
/// "❤️ 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
|
||||
|
||||
@@ -7776,6 +7595,18 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used as the first info messages in newly created classic email threads.
|
||||
#define DC_STR_CHAT_UNENCRYPTED_EXPLANATON 230
|
||||
|
||||
/// "Outgoing audio call"
|
||||
#define DC_STR_OUTGOING_AUDIO_CALL 232
|
||||
|
||||
/// "Outgoing video call"
|
||||
#define DC_STR_OUTGOING_VIDEO_CALL 233
|
||||
|
||||
/// "Incoming audio call"
|
||||
#define DC_STR_INCOMING_AUDIO_CALL 234
|
||||
|
||||
/// "Incoming video call"
|
||||
#define DC_STR_INCOMING_VIDEO_CALL 235
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
@@ -15,10 +15,9 @@ use std::collections::BTreeMap;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt::Write;
|
||||
use std::future::Future;
|
||||
use std::ops::Deref;
|
||||
use std::ptr;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use std::sync::{Arc, LazyLock, Mutex};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::Context as _;
|
||||
@@ -1182,6 +1181,7 @@ pub unsafe extern "C" fn dc_place_outgoing_call(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
place_call_info: *const libc::c_char,
|
||||
has_video: bool,
|
||||
) -> u32 {
|
||||
if context.is_null() || chat_id == 0 {
|
||||
eprintln!("ignoring careless call to dc_place_outgoing_call()");
|
||||
@@ -1191,7 +1191,7 @@ pub unsafe extern "C" fn dc_place_outgoing_call(
|
||||
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))
|
||||
block_on(ctx.place_outgoing_call(chat_id, place_call_info, has_video))
|
||||
.context("Failed to place call")
|
||||
.log_err(ctx)
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
@@ -2261,22 +2261,6 @@ pub unsafe extern "C" fn dc_get_contacts(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_blocked_cnt(context: *mut dc_context_t) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_blocked_cnt()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
Contact::get_all_blocked(ctx)
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed to get blocked count")
|
||||
.len() as libc::c_int
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_blocked_contacts(
|
||||
context: *mut dc_context_t,
|
||||
@@ -4739,33 +4723,13 @@ pub unsafe extern "C" fn dc_provider_unref(provider: *mut dc_provider_t) {
|
||||
|
||||
/// Reader-writer lock wrapper for accounts manager to guarantee thread safety when using
|
||||
/// `dc_accounts_t` in multiple threads at once.
|
||||
pub struct AccountsWrapper {
|
||||
inner: Arc<RwLock<Accounts>>,
|
||||
}
|
||||
|
||||
impl Deref for AccountsWrapper {
|
||||
type Target = Arc<RwLock<Accounts>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl AccountsWrapper {
|
||||
fn new(accounts: Accounts) -> Self {
|
||||
let inner = Arc::new(RwLock::new(accounts));
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct representing a list of deltachat accounts.
|
||||
pub type dc_accounts_t = AccountsWrapper;
|
||||
pub type dc_accounts_t = RwLock<Accounts>;
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_new(
|
||||
dir: *const libc::c_char,
|
||||
writable: libc::c_int,
|
||||
) -> *mut dc_accounts_t {
|
||||
) -> *const dc_accounts_t {
|
||||
setup_panic!();
|
||||
|
||||
if dir.is_null() {
|
||||
@@ -4776,7 +4740,99 @@ pub unsafe extern "C" fn dc_accounts_new(
|
||||
let accs = block_on(Accounts::new(as_path(dir).into(), writable != 0));
|
||||
|
||||
match accs {
|
||||
Ok(accs) => Box::into_raw(Box::new(AccountsWrapper::new(accs))),
|
||||
Ok(accs) => Arc::into_raw(Arc::new(RwLock::new(accs))),
|
||||
Err(err) => {
|
||||
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
|
||||
eprintln!("failed to create accounts: {err:#}");
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type dc_event_channel_t = Mutex<Option<Events>>;
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_event_channel_new() -> *mut dc_event_channel_t {
|
||||
Box::into_raw(Box::new(Mutex::new(Some(Events::new()))))
|
||||
}
|
||||
|
||||
/// Release the events channel structure.
|
||||
///
|
||||
/// This function releases the memory of the `dc_event_channel_t` structure.
|
||||
///
|
||||
/// you can call it after calling dc_accounts_new_with_event_channel,
|
||||
/// which took the events channel out of it already, so this just frees the underlying option.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_event_channel_unref(event_channel: *mut dc_event_channel_t) {
|
||||
if event_channel.is_null() {
|
||||
eprintln!("ignoring careless call to dc_event_channel_unref()");
|
||||
return;
|
||||
}
|
||||
drop(Box::from_raw(event_channel))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_event_channel_get_event_emitter(
|
||||
event_channel: *mut dc_event_channel_t,
|
||||
) -> *mut dc_event_emitter_t {
|
||||
if event_channel.is_null() {
|
||||
eprintln!("ignoring careless call to dc_event_channel_get_event_emitter()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let Some(event_channel) = &*(*event_channel)
|
||||
.lock()
|
||||
.expect("call to dc_event_channel_get_event_emitter() failed: mutex is poisoned")
|
||||
else {
|
||||
eprintln!(
|
||||
"ignoring careless call to dc_event_channel_get_event_emitter()
|
||||
-> channel was already consumed, make sure you call this before dc_accounts_new_with_event_channel"
|
||||
);
|
||||
return ptr::null_mut();
|
||||
};
|
||||
|
||||
let emitter = event_channel.get_emitter();
|
||||
|
||||
Box::into_raw(Box::new(emitter))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_new_with_event_channel(
|
||||
dir: *const libc::c_char,
|
||||
writable: libc::c_int,
|
||||
event_channel: *mut dc_event_channel_t,
|
||||
) -> *const dc_accounts_t {
|
||||
setup_panic!();
|
||||
|
||||
if dir.is_null() || event_channel.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_new_with_event_channel()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
// consuming channel enforce that you need to get the event emitter
|
||||
// before initializing the account manager,
|
||||
// so that you don't miss events/errors during initialisation.
|
||||
// It also prevents you from using the same channel on multiple account managers.
|
||||
let Some(event_channel) = (*event_channel)
|
||||
.lock()
|
||||
.expect("call to dc_event_channel_get_event_emitter() failed: mutex is poisoned")
|
||||
.take()
|
||||
else {
|
||||
eprintln!(
|
||||
"ignoring careless call to dc_accounts_new_with_event_channel()
|
||||
-> channel was already consumed"
|
||||
);
|
||||
return ptr::null_mut();
|
||||
};
|
||||
|
||||
let accs = block_on(Accounts::new_with_events(
|
||||
as_path(dir).into(),
|
||||
writable != 0,
|
||||
event_channel,
|
||||
));
|
||||
|
||||
match accs {
|
||||
Ok(accs) => Arc::into_raw(Arc::new(RwLock::new(accs))),
|
||||
Err(err) => {
|
||||
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
|
||||
eprintln!("failed to create accounts: {err:#}");
|
||||
@@ -4789,17 +4845,17 @@ pub unsafe extern "C" fn dc_accounts_new(
|
||||
///
|
||||
/// This function releases the memory of the `dc_accounts_t` structure.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_unref(accounts: *mut dc_accounts_t) {
|
||||
pub unsafe extern "C" fn dc_accounts_unref(accounts: *const dc_accounts_t) {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_unref()");
|
||||
return;
|
||||
}
|
||||
let _ = Box::from_raw(accounts);
|
||||
drop(Arc::from_raw(accounts));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_get_account(
|
||||
accounts: *mut dc_accounts_t,
|
||||
accounts: *const dc_accounts_t,
|
||||
id: u32,
|
||||
) -> *mut dc_context_t {
|
||||
if accounts.is_null() {
|
||||
@@ -4816,7 +4872,7 @@ pub unsafe extern "C" fn dc_accounts_get_account(
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_get_selected_account(
|
||||
accounts: *mut dc_accounts_t,
|
||||
accounts: *const dc_accounts_t,
|
||||
) -> *mut dc_context_t {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_get_selected_account()");
|
||||
@@ -4832,7 +4888,7 @@ pub unsafe extern "C" fn dc_accounts_get_selected_account(
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_select_account(
|
||||
accounts: *mut dc_accounts_t,
|
||||
accounts: *const dc_accounts_t,
|
||||
id: u32,
|
||||
) -> libc::c_int {
|
||||
if accounts.is_null() {
|
||||
@@ -4856,13 +4912,13 @@ pub unsafe extern "C" fn dc_accounts_select_account(
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_add_account(accounts: *mut dc_accounts_t) -> u32 {
|
||||
pub unsafe extern "C" fn dc_accounts_add_account(accounts: *const dc_accounts_t) -> u32 {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_add_account()");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let accounts = &mut *accounts;
|
||||
let accounts = &*accounts;
|
||||
|
||||
block_on(async move {
|
||||
let mut accounts = accounts.write().await;
|
||||
@@ -4877,13 +4933,13 @@ pub unsafe extern "C" fn dc_accounts_add_account(accounts: *mut dc_accounts_t) -
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_add_closed_account(accounts: *mut dc_accounts_t) -> u32 {
|
||||
pub unsafe extern "C" fn dc_accounts_add_closed_account(accounts: *const dc_accounts_t) -> u32 {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_add_closed_account()");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let accounts = &mut *accounts;
|
||||
let accounts = &*accounts;
|
||||
|
||||
block_on(async move {
|
||||
let mut accounts = accounts.write().await;
|
||||
@@ -4899,7 +4955,7 @@ pub unsafe extern "C" fn dc_accounts_add_closed_account(accounts: *mut dc_accoun
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_remove_account(
|
||||
accounts: *mut dc_accounts_t,
|
||||
accounts: *const dc_accounts_t,
|
||||
id: u32,
|
||||
) -> libc::c_int {
|
||||
if accounts.is_null() {
|
||||
@@ -4907,7 +4963,7 @@ pub unsafe extern "C" fn dc_accounts_remove_account(
|
||||
return 0;
|
||||
}
|
||||
|
||||
let accounts = &mut *accounts;
|
||||
let accounts = &*accounts;
|
||||
|
||||
block_on(async move {
|
||||
let mut accounts = accounts.write().await;
|
||||
@@ -4925,7 +4981,7 @@ pub unsafe extern "C" fn dc_accounts_remove_account(
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_migrate_account(
|
||||
accounts: *mut dc_accounts_t,
|
||||
accounts: *const dc_accounts_t,
|
||||
dbfile: *const libc::c_char,
|
||||
) -> u32 {
|
||||
if accounts.is_null() || dbfile.is_null() {
|
||||
@@ -4933,7 +4989,7 @@ pub unsafe extern "C" fn dc_accounts_migrate_account(
|
||||
return 0;
|
||||
}
|
||||
|
||||
let accounts = &mut *accounts;
|
||||
let accounts = &*accounts;
|
||||
let dbfile = to_string_lossy(dbfile);
|
||||
|
||||
block_on(async move {
|
||||
@@ -4954,7 +5010,7 @@ pub unsafe extern "C" fn dc_accounts_migrate_account(
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_get_all(accounts: *mut dc_accounts_t) -> *mut dc_array_t {
|
||||
pub unsafe extern "C" fn dc_accounts_get_all(accounts: *const dc_accounts_t) -> *mut dc_array_t {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_get_all()");
|
||||
return ptr::null_mut();
|
||||
@@ -4968,18 +5024,18 @@ pub unsafe extern "C" fn dc_accounts_get_all(accounts: *mut dc_accounts_t) -> *m
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_start_io(accounts: *mut dc_accounts_t) {
|
||||
pub unsafe extern "C" fn dc_accounts_start_io(accounts: *const dc_accounts_t) {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_start_io()");
|
||||
return;
|
||||
}
|
||||
|
||||
let accounts = &mut *accounts;
|
||||
let accounts = &*accounts;
|
||||
block_on(async move { accounts.write().await.start_io().await });
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_stop_io(accounts: *mut dc_accounts_t) {
|
||||
pub unsafe extern "C" fn dc_accounts_stop_io(accounts: *const dc_accounts_t) {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_stop_io()");
|
||||
return;
|
||||
@@ -4990,7 +5046,7 @@ pub unsafe extern "C" fn dc_accounts_stop_io(accounts: *mut dc_accounts_t) {
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_maybe_network(accounts: *mut dc_accounts_t) {
|
||||
pub unsafe extern "C" fn dc_accounts_maybe_network(accounts: *const dc_accounts_t) {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_maybe_network()");
|
||||
return;
|
||||
@@ -5001,7 +5057,7 @@ pub unsafe extern "C" fn dc_accounts_maybe_network(accounts: *mut dc_accounts_t)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *mut dc_accounts_t) {
|
||||
pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *const dc_accounts_t) {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_maybe_network_lost()");
|
||||
return;
|
||||
@@ -5013,7 +5069,7 @@ pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *mut dc_accoun
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_background_fetch(
|
||||
accounts: *mut dc_accounts_t,
|
||||
accounts: *const dc_accounts_t,
|
||||
timeout_in_seconds: u64,
|
||||
) -> libc::c_int {
|
||||
if accounts.is_null() || timeout_in_seconds <= 2 {
|
||||
@@ -5032,7 +5088,7 @@ pub unsafe extern "C" fn dc_accounts_background_fetch(
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_stop_background_fetch(accounts: *mut dc_accounts_t) {
|
||||
pub unsafe extern "C" fn dc_accounts_stop_background_fetch(accounts: *const dc_accounts_t) {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_stop_background_fetch()");
|
||||
return;
|
||||
@@ -5044,7 +5100,7 @@ pub unsafe extern "C" fn dc_accounts_stop_background_fetch(accounts: *mut dc_acc
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_set_push_device_token(
|
||||
accounts: *mut dc_accounts_t,
|
||||
accounts: *const dc_accounts_t,
|
||||
token: *const libc::c_char,
|
||||
) {
|
||||
if accounts.is_null() {
|
||||
@@ -5067,7 +5123,7 @@ pub unsafe extern "C" fn dc_accounts_set_push_device_token(
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_get_event_emitter(
|
||||
accounts: *mut dc_accounts_t,
|
||||
accounts: *const dc_accounts_t,
|
||||
) -> *mut dc_event_emitter_t {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_get_event_emitter()");
|
||||
@@ -5087,16 +5143,16 @@ pub struct dc_jsonrpc_instance_t {
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_jsonrpc_init(
|
||||
account_manager: *mut dc_accounts_t,
|
||||
account_manager: *const dc_accounts_t,
|
||||
) -> *mut dc_jsonrpc_instance_t {
|
||||
if account_manager.is_null() {
|
||||
eprintln!("ignoring careless call to dc_jsonrpc_init()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let account_manager = &*account_manager;
|
||||
let account_manager = Arc::from_raw(account_manager);
|
||||
let cmd_api = block_on(deltachat_jsonrpc::api::CommandApi::from_arc(
|
||||
account_manager.inner.clone(),
|
||||
account_manager.clone(),
|
||||
));
|
||||
|
||||
let (request_handle, receiver) = RpcClient::new();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.34.0"
|
||||
version = "2.41.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
@@ -19,7 +19,6 @@ yerpc = { workspace = true, features = ["anyhow_expose", "openrpc"] }
|
||||
typescript-type-def = { version = "0.5.13", features = ["json_value"] }
|
||||
tokio = { workspace = true }
|
||||
sanitize-filename = { workspace = true }
|
||||
walkdir = "2.5.0"
|
||||
base64 = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -11,11 +11,11 @@ use deltachat::blob::BlobObject;
|
||||
use deltachat::calls::ice_servers;
|
||||
use deltachat::chat::{
|
||||
self, add_contact_to_chat, forward_msgs, forward_msgs_2ctx, get_chat_media, get_chat_msgs,
|
||||
get_chat_msgs_ex, marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem,
|
||||
MessageListOptions,
|
||||
get_chat_msgs_ex, marknoticed_all_chats, marknoticed_chat, remove_contact_from_chat, Chat,
|
||||
ChatId, ChatItem, MessageListOptions,
|
||||
};
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::config::Config;
|
||||
use deltachat::config::{get_all_ui_config_keys, Config};
|
||||
use deltachat::constants::DC_MSG_ID_DAYMARKER;
|
||||
use deltachat::contact::{may_be_valid_addr, Contact, ContactId, Origin};
|
||||
use deltachat::context::get_info;
|
||||
@@ -23,8 +23,8 @@ use deltachat::ephemeral::Timer;
|
||||
use deltachat::imex;
|
||||
use deltachat::location;
|
||||
use deltachat::message::{
|
||||
self, delete_msgs_ex, get_existing_msg_ids, get_msg_read_receipts, markseen_msgs, Message,
|
||||
MessageState, MsgId, Viewtype,
|
||||
self, delete_msgs_ex, get_existing_msg_ids, get_msg_read_receipt_count, get_msg_read_receipts,
|
||||
markseen_msgs, Message, MessageState, MsgId, Viewtype,
|
||||
};
|
||||
use deltachat::peer_channels::{
|
||||
leave_webxdc_realtime, send_webxdc_realtime_advertisement, send_webxdc_realtime_data,
|
||||
@@ -35,14 +35,13 @@ use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
|
||||
use deltachat::reaction::{get_msg_reactions, send_reaction};
|
||||
use deltachat::securejoin;
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::storage_usage::get_storage_usage;
|
||||
use deltachat::storage_usage::{get_blobdir_storage_usage, get_storage_usage};
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::EventEmitter;
|
||||
use sanitize_filename::is_sanitized;
|
||||
use tokio::fs;
|
||||
use tokio::sync::{watch, Mutex, RwLock};
|
||||
use types::login_param::EnteredLoginParam;
|
||||
use walkdir::WalkDir;
|
||||
use yerpc::rpc;
|
||||
|
||||
pub mod types;
|
||||
@@ -330,13 +329,7 @@ impl CommandApi {
|
||||
async fn get_account_file_size(&self, account_id: u32) -> Result<u64> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let dbfile = ctx.get_dbfile().metadata()?.len();
|
||||
let total_size = WalkDir::new(ctx.get_blobdir())
|
||||
.max_depth(2)
|
||||
.into_iter()
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter_map(|entry| entry.metadata().ok())
|
||||
.filter(|metadata| metadata.is_file())
|
||||
.fold(0, |acc, m| acc + m.len());
|
||||
let total_size = get_blobdir_storage_usage(&ctx);
|
||||
|
||||
Ok(dbfile + total_size)
|
||||
}
|
||||
@@ -423,11 +416,11 @@ impl CommandApi {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set configuration values from a QR code. (technically from the URI that is stored in the qrcode)
|
||||
/// Before this function is called, `checkQr()` should confirm the type of the
|
||||
/// QR code is `account` or `webrtcInstance`.
|
||||
/// Set configuration values from a QR code (technically from the URI stored in it).
|
||||
/// Before this function is called, `check_qr()` should be used to get the QR code type.
|
||||
///
|
||||
/// Internally, the function will call dc_set_config() with the appropriate keys,
|
||||
/// "DCACCOUNT:" and "DCLOGIN:" QR codes configure the account, but I/O mustn't be started for
|
||||
/// such QR codes, consider using [`Self::add_transport_from_qr`] which also restarts I/O.
|
||||
async fn set_config_from_qr(&self, account_id: u32, qr_content: String) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
qr::set_config_from_qr(&ctx, &qr_content).await
|
||||
@@ -459,6 +452,12 @@ impl CommandApi {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Returns all `ui.*` config keys that were set by the UI.
|
||||
async fn get_all_ui_config_keys(&self, account_id: u32) -> Result<Vec<String>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
get_all_ui_config_keys(&ctx).await
|
||||
}
|
||||
|
||||
async fn set_stock_strings(&self, strings: HashMap<u32, String>) -> Result<()> {
|
||||
let accounts = self.accounts.read().await;
|
||||
for (stock_id, stock_message) in strings {
|
||||
@@ -802,11 +801,11 @@ impl CommandApi {
|
||||
/// Delete a chat.
|
||||
///
|
||||
/// Messages are deleted from the device and the chat database entry is deleted.
|
||||
/// After that, the event #DC_EVENT_MSGS_CHANGED is posted.
|
||||
/// After that, a `MsgsChanged` event is emitted.
|
||||
/// Messages are deleted from the server in background.
|
||||
///
|
||||
/// Things that are _not done_ implicitly:
|
||||
///
|
||||
/// - Messages are **not deleted from the server**.
|
||||
/// - The chat or the contact is **not blocked**, so new messages from the user/the group may appear as a contact request
|
||||
/// and the user may create the chat again.
|
||||
/// - **Groups are not left** - this would
|
||||
@@ -1165,10 +1164,24 @@ impl CommandApi {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Mark all messages in all chats as _noticed_.
|
||||
/// Skips messages from blocked contacts, but does not skip messages in muted chats.
|
||||
///
|
||||
/// _Noticed_ messages are no longer _fresh_ and do not count as being unseen
|
||||
/// but are still waiting for being marked as "seen" using markseen_msgs()
|
||||
/// (read receipts aren't sent for noticed messages).
|
||||
///
|
||||
/// Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED.
|
||||
/// See also markseen_msgs().
|
||||
pub async fn marknoticed_all_chats(&self, account_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
marknoticed_all_chats(&ctx).await
|
||||
}
|
||||
|
||||
/// Mark all messages in a chat as _noticed_.
|
||||
/// _Noticed_ messages are no longer _fresh_ and do not count as being unseen
|
||||
/// but are still waiting for being marked as "seen" using markseen_msgs()
|
||||
/// (IMAP/MDNs is not done for noticed messages).
|
||||
/// (read receipts aren't sent for noticed messages).
|
||||
///
|
||||
/// Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED.
|
||||
/// See also markseen_msgs().
|
||||
@@ -1435,6 +1448,18 @@ impl CommandApi {
|
||||
MessageInfo::from_msg_id(&ctx, MsgId::new(message_id)).await
|
||||
}
|
||||
|
||||
/// Returns count of read receipts on message.
|
||||
///
|
||||
/// This view count is meant as a feedback measure for the channel owner only.
|
||||
async fn get_message_read_receipt_count(
|
||||
&self,
|
||||
account_id: u32,
|
||||
message_id: u32,
|
||||
) -> Result<usize> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
get_msg_read_receipt_count(&ctx, MsgId::new(message_id)).await
|
||||
}
|
||||
|
||||
/// Returns contacts that sent read receipts and the time of reading.
|
||||
async fn get_message_read_receipts(
|
||||
&self,
|
||||
@@ -2142,10 +2167,11 @@ impl CommandApi {
|
||||
account_id: u32,
|
||||
chat_id: u32,
|
||||
place_call_info: String,
|
||||
has_video: bool,
|
||||
) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let msg_id = ctx
|
||||
.place_outgoing_call(ChatId::new(chat_id), place_call_info)
|
||||
.place_outgoing_call(ChatId::new(chat_id), place_call_info, has_video)
|
||||
.await?;
|
||||
Ok(msg_id.to_u32())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
|
||||
use deltachat::calls::{call_state, sdp_has_video, CallState};
|
||||
use deltachat::calls::{call_state, CallState};
|
||||
use deltachat::context::Context;
|
||||
use deltachat::message::MsgId;
|
||||
use serde::Serialize;
|
||||
@@ -15,7 +15,7 @@ pub struct JsonrpcCallInfo {
|
||||
/// even if incoming call event was missed.
|
||||
pub sdp_offer: String,
|
||||
|
||||
/// True if SDP offer has a video.
|
||||
/// True if the call is started as a video call.
|
||||
pub has_video: bool,
|
||||
|
||||
/// Call state.
|
||||
@@ -30,7 +30,7 @@ impl JsonrpcCallInfo {
|
||||
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 has_video = call_info.has_video_initially();
|
||||
let state = JsonrpcCallState::from_msg_id(context, msg_id).await?;
|
||||
|
||||
Ok(JsonrpcCallInfo {
|
||||
|
||||
@@ -10,7 +10,6 @@ use serde::{Deserialize, Serialize};
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
use super::color_int_to_hex_string;
|
||||
use super::contact::ContactObject;
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -48,7 +47,6 @@ pub struct FullChat {
|
||||
chat_type: JsonrpcChatType,
|
||||
is_unpromoted: bool,
|
||||
is_self_talk: bool,
|
||||
contacts: Vec<ContactObject>,
|
||||
contact_ids: Vec<u32>,
|
||||
|
||||
/// Contact IDs of the past chat members.
|
||||
@@ -83,20 +81,6 @@ impl FullChat {
|
||||
let contact_ids = get_chat_contacts(context, rust_chat_id).await?;
|
||||
let past_contact_ids = get_past_chat_contacts(context, rust_chat_id).await?;
|
||||
|
||||
let mut contacts = Vec::with_capacity(contact_ids.len());
|
||||
|
||||
for contact_id in &contact_ids {
|
||||
contacts.push(
|
||||
ContactObject::try_from_dc_contact(
|
||||
context,
|
||||
Contact::get_by_id(context, *contact_id)
|
||||
.await
|
||||
.context("failed to load contact")?,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
let profile_image = match chat.get_profile_image(context).await? {
|
||||
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
|
||||
None => None,
|
||||
@@ -132,7 +116,6 @@ impl FullChat {
|
||||
chat_type: chat.get_type().into(),
|
||||
is_unpromoted: chat.is_unpromoted(),
|
||||
is_self_talk: chat.is_self_talk(),
|
||||
contacts,
|
||||
contact_ids: contact_ids.iter().map(|id| id.to_u32()).collect(),
|
||||
past_contact_ids: past_contact_ids.iter().map(|id| id.to_u32()).collect(),
|
||||
color,
|
||||
@@ -150,7 +133,6 @@ impl FullChat {
|
||||
}
|
||||
|
||||
/// cheaper version of fullchat, omits:
|
||||
/// - contacts
|
||||
/// - contact_ids
|
||||
/// - fresh_message_counter
|
||||
/// - ephemeral_timer
|
||||
|
||||
@@ -47,8 +47,7 @@ pub struct ContactObject {
|
||||
///
|
||||
/// - If `verifierId` != 0,
|
||||
/// display text "Introduced by ..."
|
||||
/// with the name and address of the contact
|
||||
/// formatted by `name_and_addr`/`nameAndAddr`.
|
||||
/// with the name of the contact.
|
||||
/// Prefix the text by a green checkmark.
|
||||
///
|
||||
/// - If `verifierId` == 0 and `isVerified` != 0,
|
||||
|
||||
@@ -463,11 +463,11 @@ pub enum EventType {
|
||||
|
||||
/// One or more transports has changed.
|
||||
///
|
||||
/// This event is used for tests to detect when transport
|
||||
/// synchronization messages arrives.
|
||||
/// UIs don't need to use it, it is unlikely
|
||||
/// that user modifies transports on multiple
|
||||
/// devices simultaneously.
|
||||
/// UI should update the list.
|
||||
///
|
||||
/// This event is emitted when transport
|
||||
/// synchronization messages arrives,
|
||||
/// but not when the UI modifies the transport list by itself.
|
||||
TransportsModified,
|
||||
}
|
||||
|
||||
|
||||
@@ -92,6 +92,9 @@ pub struct MessageObject {
|
||||
|
||||
file: Option<String>,
|
||||
file_mime: Option<String>,
|
||||
|
||||
/// The size of the file in bytes, if applicable.
|
||||
/// If message is a pre-message, then this is the size of the file to be downloaded.
|
||||
file_bytes: u64,
|
||||
file_name: Option<String>,
|
||||
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.34.0"
|
||||
"version": "2.41.0"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "2.34.0"
|
||||
version = "2.41.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
@@ -1231,7 +1231,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"setqr" => {
|
||||
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'"),
|
||||
Ok(()) => eprintln!("Config set from the QR code."),
|
||||
Err(err) => eprintln!("Cannot set config from QR code: {err:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,12 +430,12 @@ async fn handle_cmd(
|
||||
}
|
||||
"oauth2" => {
|
||||
if let Some(addr) = ctx.get_config(config::Config::Addr).await? {
|
||||
let oauth2_url =
|
||||
get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await?;
|
||||
if oauth2_url.is_none() {
|
||||
println!("OAuth2 not available for {}.", &addr);
|
||||
if let Some(oauth2_url) =
|
||||
get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await?
|
||||
{
|
||||
println!("Open the following url, set mail_pw to the generated token and server_flags to 2:\n{oauth2_url}");
|
||||
} else {
|
||||
println!("Open the following url, set mail_pw to the generated token and server_flags to 2:\n{}", oauth2_url.unwrap());
|
||||
println!("OAuth2 not available for {}.", &addr);
|
||||
}
|
||||
} else {
|
||||
println!("oauth2: set addr first.");
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "2.34.0"
|
||||
version = "2.41.0"
|
||||
license = "MPL-2.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
|
||||
@@ -44,8 +44,13 @@ class AttrDict(dict):
|
||||
super().__setattr__(attr, val)
|
||||
|
||||
|
||||
def _forever(_event: AttrDict) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def run_client_cli(
|
||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||
until: Callable[[AttrDict], bool] = _forever,
|
||||
argv: Optional[list] = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
@@ -55,10 +60,11 @@ def run_client_cli(
|
||||
"""
|
||||
from .client import Client
|
||||
|
||||
_run_cli(Client, hooks, argv, **kwargs)
|
||||
_run_cli(Client, until, hooks, argv, **kwargs)
|
||||
|
||||
|
||||
def run_bot_cli(
|
||||
until: Callable[[AttrDict], bool] = _forever,
|
||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||
argv: Optional[list] = None,
|
||||
**kwargs,
|
||||
@@ -69,11 +75,12 @@ def run_bot_cli(
|
||||
"""
|
||||
from .client import Bot
|
||||
|
||||
_run_cli(Bot, hooks, argv, **kwargs)
|
||||
_run_cli(Bot, until, hooks, argv, **kwargs)
|
||||
|
||||
|
||||
def _run_cli(
|
||||
client_type: Type["Client"],
|
||||
until: Callable[[AttrDict], bool] = _forever,
|
||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||
argv: Optional[list] = None,
|
||||
**kwargs,
|
||||
@@ -111,7 +118,7 @@ def _run_cli(
|
||||
kwargs={"email": args.email, "password": args.password},
|
||||
)
|
||||
configure_thread.start()
|
||||
client.run_forever()
|
||||
client.run_until(until)
|
||||
|
||||
|
||||
def extract_addr(text: str) -> str:
|
||||
|
||||
@@ -303,7 +303,7 @@ class Chat:
|
||||
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:
|
||||
def place_outgoing_call(self, place_call_info: str, has_video_initially: bool) -> Message:
|
||||
"""Starts an outgoing call."""
|
||||
msg_id = self._rpc.place_outgoing_call(self.account.id, self.id, place_call_info)
|
||||
msg_id = self._rpc.place_outgoing_call(self.account.id, self.id, place_call_info, has_video_initially)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
@@ -14,6 +14,7 @@ from typing import (
|
||||
|
||||
from ._utils import (
|
||||
AttrDict,
|
||||
_forever,
|
||||
parse_system_add_remove,
|
||||
parse_system_image_changed,
|
||||
parse_system_title_changed,
|
||||
@@ -91,19 +92,28 @@ class Client:
|
||||
|
||||
def run_forever(self) -> None:
|
||||
"""Process events forever."""
|
||||
self.run_until(lambda _: False)
|
||||
self.run_until(_forever)
|
||||
|
||||
def run_until(self, func: Callable[[AttrDict], bool]) -> AttrDict:
|
||||
"""Process events until the given callable evaluates to True.
|
||||
|
||||
The callable should accept an AttrDict object representing the
|
||||
last processed event. The event is returned when the callable
|
||||
evaluates to True.
|
||||
"""
|
||||
"""Start the event processing loop."""
|
||||
self.logger.debug("Listening to incoming events...")
|
||||
if self.is_configured():
|
||||
self.account.start_io()
|
||||
self._process_messages() # Process old messages.
|
||||
return self._process_events(until_func=func) # Loop over incoming events
|
||||
|
||||
def _process_events(
|
||||
self,
|
||||
until_func: Callable[[AttrDict], bool] = _forever,
|
||||
until_event: EventType = False,
|
||||
) -> AttrDict:
|
||||
"""Process events until the given callable evaluates to True,
|
||||
or until a certain event happens.
|
||||
|
||||
The until_func callable should accept an AttrDict object representing
|
||||
the last processed event. The event is returned when the callable
|
||||
evaluates to True.
|
||||
"""
|
||||
while True:
|
||||
event = self.account.wait_for_event()
|
||||
event["kind"] = EventType(event.kind)
|
||||
@@ -112,10 +122,13 @@ class Client:
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
self._process_messages()
|
||||
|
||||
stop = func(event)
|
||||
stop = until_func(event)
|
||||
if stop:
|
||||
return event
|
||||
|
||||
if event.kind == until_event:
|
||||
return event
|
||||
|
||||
def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None:
|
||||
for hook, evfilter in self._hooks.get(filter_type, []):
|
||||
if evfilter.filter(event):
|
||||
|
||||
@@ -44,6 +44,14 @@ class Message:
|
||||
read_receipts = self._rpc.get_message_read_receipts(self.account.id, self.id)
|
||||
return [AttrDict(read_receipt) for read_receipt in read_receipts]
|
||||
|
||||
def get_read_receipt_count(self) -> int:
|
||||
"""
|
||||
Returns count of read receipts on message.
|
||||
|
||||
This view count is meant as a feedback measure for the channel owner only.
|
||||
"""
|
||||
return self._rpc.get_message_read_receipt_count(self.account.id, self.id)
|
||||
|
||||
def get_reactions(self) -> Optional[AttrDict]:
|
||||
"""Get message reactions."""
|
||||
reactions = self._rpc.get_message_reactions(self.account.id, self.id)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
@@ -21,7 +22,7 @@ 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".
|
||||
Currently this is "Messages are end-to-end encrypted."
|
||||
"""
|
||||
|
||||
|
||||
@@ -204,14 +205,13 @@ def log():
|
||||
|
||||
class Printer:
|
||||
def section(self, msg: str) -> None:
|
||||
print()
|
||||
print("=" * 10, msg, "=" * 10)
|
||||
logging.info("\n%s %s %s", "=" * 10, msg, "=" * 10)
|
||||
|
||||
def step(self, msg: str) -> None:
|
||||
print("-" * 5, "step " + msg, "-" * 5)
|
||||
logging.info("%s step %s %s", "-" * 5, msg, "-" * 5)
|
||||
|
||||
def indent(self, msg: str) -> None:
|
||||
print(" " + msg)
|
||||
logging.info(" " + msg)
|
||||
|
||||
return Printer()
|
||||
|
||||
@@ -261,7 +261,7 @@ def get_core_python_env(tmp_path_factory):
|
||||
envs[core_version] = venv
|
||||
python = find_path(venv, "python")
|
||||
rpc_server_path = find_path(venv, "deltachat-rpc-server")
|
||||
print(f"python={python}\nrpc_server={rpc_server_path}")
|
||||
logging.info(f"Paths:\npython={python}\nrpc_server={rpc_server_path}")
|
||||
return python, rpc_server_path
|
||||
|
||||
return get_versioned_venv
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import imaplib
|
||||
import io
|
||||
import logging
|
||||
import pathlib
|
||||
import ssl
|
||||
from contextlib import contextmanager
|
||||
@@ -45,13 +46,13 @@ class DirectImap:
|
||||
try:
|
||||
self.conn.logout()
|
||||
except (OSError, imaplib.IMAP4.abort):
|
||||
print("Could not logout direct_imap conn")
|
||||
logging.warning("Could not logout direct_imap conn")
|
||||
|
||||
def create_folder(self, foldername):
|
||||
try:
|
||||
self.conn.folder.create(foldername)
|
||||
except errors.MailboxFolderCreateError as e:
|
||||
print("Can't create", foldername, "probably it already exists:", str(e))
|
||||
logging.warning(f"Cannot create '{foldername}', probably it already exists: {str(e)}")
|
||||
|
||||
def select_folder(self, foldername: str) -> tuple:
|
||||
assert not self._idling
|
||||
@@ -95,7 +96,7 @@ class DirectImap:
|
||||
messages = self.get_unread_messages()
|
||||
if messages:
|
||||
res = self.conn.flag(messages, MailMessageFlags.SEEN, True)
|
||||
print("marked seen:", messages, res)
|
||||
logging.info(f"Marked seen: {messages} {res}")
|
||||
|
||||
def get_unread_cnt(self) -> int:
|
||||
return len(self.get_unread_messages())
|
||||
|
||||
@@ -10,15 +10,15 @@ def test_calls(acfactory) -> None:
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
|
||||
outgoing_call_message = alice_chat_bob.place_outgoing_call(place_call_info)
|
||||
outgoing_call_message = alice_chat_bob.place_outgoing_call(place_call_info, has_video_initially=True)
|
||||
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
|
||||
assert incoming_call_event.has_video
|
||||
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
|
||||
assert 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
|
||||
@@ -41,46 +41,38 @@ def test_video_call(acfactory) -> None:
|
||||
#
|
||||
# `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)
|
||||
|
||||
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
|
||||
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)
|
||||
alice_chat_bob.place_outgoing_call("offer", has_video_initially=True)
|
||||
|
||||
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
|
||||
assert incoming_call_event.place_call_info == place_call_info
|
||||
assert incoming_call_event.place_call_info == "offer"
|
||||
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_audio_call(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.place_outgoing_call("offer", has_video_initially=False)
|
||||
|
||||
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
|
||||
assert incoming_call_event.place_call_info == "offer"
|
||||
assert not incoming_call_event.has_video
|
||||
|
||||
incoming_call_message = Message(bob, incoming_call_event.msg_id)
|
||||
assert not incoming_call_message.get_call_info().has_video
|
||||
|
||||
|
||||
def test_ice_servers(acfactory) -> None:
|
||||
alice = acfactory.get_online_account()
|
||||
|
||||
@@ -92,7 +84,7 @@ def test_no_contact_request_call(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
alice_chat_bob.place_outgoing_call("offer")
|
||||
alice_chat_bob.place_outgoing_call("offer", has_video_initially=True)
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
# Notification for "Hello!" message should arrive
|
||||
@@ -107,3 +99,48 @@ def test_no_contact_request_call(acfactory) -> None:
|
||||
msg = bob.get_message_by_id(event.msg_id)
|
||||
if msg.get_snapshot().text == "Hello!":
|
||||
break
|
||||
|
||||
|
||||
def test_who_can_call_me_nobody(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
# Bob sets "who can call me" to "nobody" (2)
|
||||
bob.set_config("who_can_call_me", "2")
|
||||
|
||||
# Bob even accepts Alice in advance so the chat does not appear as contact request.
|
||||
bob.create_chat(alice)
|
||||
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
alice_chat_bob.place_outgoing_call("offer", has_video_initially=True)
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
# Notification for "Hello!" message should arrive
|
||||
# without the call ringing.
|
||||
while True:
|
||||
event = bob.wait_for_event()
|
||||
|
||||
# There should be no incoming call notification.
|
||||
assert event.kind != EventType.INCOMING_CALL
|
||||
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
msg = bob.get_message_by_id(event.msg_id)
|
||||
if msg.get_snapshot().text == "Hello!":
|
||||
break
|
||||
|
||||
|
||||
def test_who_can_call_me_everybody(acfactory) -> None:
|
||||
"""Test that if "who can call me" setting is set to "everybody", calls arrive even in contact request chats."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
# Bob sets "who can call me" to "nobody" (0)
|
||||
bob.set_config("who_can_call_me", "0")
|
||||
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
alice_chat_bob.place_outgoing_call("offer", has_video_initially=True)
|
||||
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
|
||||
|
||||
incoming_call_message = Message(bob, incoming_call_event.msg_id)
|
||||
|
||||
# Even with the call arriving, the chat is still in the contact request mode.
|
||||
incoming_chat = incoming_call_message.get_snapshot().chat
|
||||
assert incoming_chat.get_basic_snapshot().is_contact_request
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from deltachat_rpc_client import Account, EventType, const
|
||||
@@ -129,7 +127,7 @@ def test_download_on_demand(acfactory: ACFactory) -> None:
|
||||
msg.get_snapshot().chat.accept()
|
||||
bob.get_chat_by_id(chat_id).send_message(
|
||||
"Hello World, this message is bigger than 5 bytes",
|
||||
html=base64.b64encode(os.urandom(300000)).decode("utf-8"),
|
||||
file="../test-data/image/screenshot.jpg",
|
||||
)
|
||||
|
||||
message = alice.wait_for_incoming_msg()
|
||||
|
||||
@@ -8,8 +8,10 @@ from imap_tools import AND, U
|
||||
from deltachat_rpc_client import Contact, EventType, Message
|
||||
|
||||
|
||||
def test_move_works(acfactory):
|
||||
def test_move_works(acfactory, direct_imap):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac2_direct_imap = direct_imap(ac2)
|
||||
ac2_direct_imap.create_folder("DeltaChat")
|
||||
ac2.set_config("mvbox_move", "1")
|
||||
ac2.bring_online()
|
||||
|
||||
@@ -34,6 +36,8 @@ def test_move_avoids_loop(acfactory, direct_imap):
|
||||
We do not want to move this message from `INBOX.DeltaChat` to `DeltaChat` again.
|
||||
"""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac2_direct_imap = direct_imap(ac2)
|
||||
ac2_direct_imap.create_folder("DeltaChat")
|
||||
ac2.set_config("mvbox_move", "1")
|
||||
ac2.set_config("delete_server_after", "0")
|
||||
ac2.bring_online()
|
||||
@@ -97,6 +101,8 @@ def test_reactions_for_a_reordering_move(acfactory, direct_imap):
|
||||
addr, password = acfactory.get_credentials()
|
||||
ac2 = acfactory.get_unconfigured_account()
|
||||
ac2.add_or_update_transport({"addr": addr, "password": password})
|
||||
ac2_direct_imap = direct_imap(ac2)
|
||||
ac2_direct_imap.create_folder("DeltaChat")
|
||||
ac2.set_config("mvbox_move", "1")
|
||||
assert ac2.is_configured()
|
||||
|
||||
@@ -130,30 +136,6 @@ def test_reactions_for_a_reordering_move(acfactory, direct_imap):
|
||||
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
|
||||
|
||||
|
||||
def test_delete_deltachat_folder(acfactory, direct_imap):
|
||||
"""Test that DeltaChat folder is recreated if user deletes it manually."""
|
||||
ac1 = acfactory.new_configured_account()
|
||||
ac1.set_config("mvbox_move", "1")
|
||||
ac1.bring_online()
|
||||
|
||||
ac1_direct_imap = direct_imap(ac1)
|
||||
ac1_direct_imap.conn.folder.delete("DeltaChat")
|
||||
assert "DeltaChat" not in ac1_direct_imap.list_folders()
|
||||
|
||||
# Wait until new folder is created and UIDVALIDITY is updated.
|
||||
while True:
|
||||
event = ac1.wait_for_event()
|
||||
if event.kind == EventType.INFO and "transport 1: UID validity for folder DeltaChat changed from " in event.msg:
|
||||
break
|
||||
|
||||
ac2 = acfactory.get_online_account()
|
||||
ac2.create_chat(ac1).send_text("hello")
|
||||
msg = ac1.wait_for_incoming_msg().get_snapshot()
|
||||
assert msg.text == "hello"
|
||||
|
||||
assert "DeltaChat" in ac1_direct_imap.list_folders()
|
||||
|
||||
|
||||
def test_dont_show_emails(acfactory, direct_imap, log):
|
||||
"""Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them.
|
||||
So: If it's outgoing AND there is no Received header, then ignore the email.
|
||||
@@ -294,10 +276,12 @@ def test_dont_show_emails(acfactory, direct_imap, log):
|
||||
assert len(msg.chat.get_messages()) == 2
|
||||
|
||||
|
||||
def test_move_works_on_self_sent(acfactory):
|
||||
def test_move_works_on_self_sent(acfactory, direct_imap):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
# Enable movebox and wait until it is created.
|
||||
# Create and enable movebox.
|
||||
ac1_direct_imap = direct_imap(ac1)
|
||||
ac1_direct_imap.create_folder("DeltaChat")
|
||||
ac1.set_config("mvbox_move", "1")
|
||||
ac1.set_config("bcc_self", "1")
|
||||
ac1.bring_online()
|
||||
@@ -314,6 +298,8 @@ def test_move_works_on_self_sent(acfactory):
|
||||
def test_moved_markseen(acfactory, direct_imap):
|
||||
"""Test that message already moved to DeltaChat folder is marked as seen."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac2_direct_imap = direct_imap(ac2)
|
||||
ac2_direct_imap.create_folder("DeltaChat")
|
||||
ac2.set_config("mvbox_move", "1")
|
||||
ac2.set_config("delete_server_after", "0")
|
||||
ac2.set_config("sync_msgs", "0") # Do not send a sync message when accepting a contact request.
|
||||
@@ -356,6 +342,8 @@ def test_markseen_message_and_mdn(acfactory, direct_imap, mvbox_move):
|
||||
for ac in ac1, ac2:
|
||||
ac.set_config("delete_server_after", "0")
|
||||
if mvbox_move:
|
||||
ac_direct_imap = direct_imap(ac)
|
||||
ac_direct_imap.create_folder("DeltaChat")
|
||||
ac.set_config("mvbox_move", "1")
|
||||
ac.bring_online()
|
||||
|
||||
@@ -390,31 +378,6 @@ def test_markseen_message_and_mdn(acfactory, direct_imap, mvbox_move):
|
||||
assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 1
|
||||
|
||||
|
||||
def test_mvbox_and_trash(acfactory, direct_imap, log):
|
||||
log.section("ac1: start with mvbox")
|
||||
ac1 = acfactory.get_online_account()
|
||||
ac1.set_config("mvbox_move", "1")
|
||||
ac1.bring_online()
|
||||
|
||||
log.section("ac2: start without a mvbox")
|
||||
ac2 = acfactory.get_online_account()
|
||||
|
||||
log.section("ac1: create trash")
|
||||
ac1_direct_imap = direct_imap(ac1)
|
||||
ac1_direct_imap.create_folder("Trash")
|
||||
ac1.set_config("scan_all_folders_debounce_secs", "0")
|
||||
ac1.stop_io()
|
||||
ac1.start_io()
|
||||
|
||||
log.section("ac1: send message and wait for ac2 to receive it")
|
||||
acfactory.get_accepted_chat(ac1, ac2).send_text("message1")
|
||||
assert ac2.wait_for_incoming_msg().get_snapshot().text == "message1"
|
||||
|
||||
assert ac1.get_config("configured_mvbox_folder") == "DeltaChat"
|
||||
while ac1.get_config("configured_trash_folder") != "Trash":
|
||||
ac1.wait_for_event(EventType.CONNECTIVITY_CHANGED)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("folder", "move", "expected_destination"),
|
||||
[
|
||||
@@ -490,20 +453,8 @@ def test_trash_multiple_messages(acfactory, direct_imap, log):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac2.stop_io()
|
||||
|
||||
# Create the Trash folder on IMAP server and configure deletion to it. There was a bug that if
|
||||
# Trash wasn't configured initially, it can't be configured later, let's check this.
|
||||
log.section("Creating trash folder")
|
||||
ac2_direct_imap = direct_imap(ac2)
|
||||
ac2_direct_imap.create_folder("Trash")
|
||||
ac2.set_config("delete_server_after", "0")
|
||||
ac2.set_config("sync_msgs", "0")
|
||||
ac2.set_config("delete_to_trash", "1")
|
||||
|
||||
log.section("Check that Trash can be configured initially as well")
|
||||
ac3 = ac2.clone()
|
||||
ac3.bring_online()
|
||||
assert ac3.get_config("configured_trash_folder")
|
||||
ac3.stop_io()
|
||||
|
||||
ac2.start_io()
|
||||
chat12 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
@@ -520,17 +471,15 @@ def test_trash_multiple_messages(acfactory, direct_imap, log):
|
||||
assert msg.text in texts
|
||||
if text != "second":
|
||||
to_delete.append(msg)
|
||||
# ac2 has received some messages, this is impossible w/o the trash folder configured, let's
|
||||
# check the configuration.
|
||||
assert ac2.get_config("configured_trash_folder") == "Trash"
|
||||
|
||||
log.section("ac2: deleting all messages except second")
|
||||
assert len(to_delete) == len(texts) - 1
|
||||
ac2.delete_messages(to_delete)
|
||||
|
||||
log.section("ac2: test that only one message is left")
|
||||
ac2_direct_imap = direct_imap(ac2)
|
||||
while 1:
|
||||
ac2.wait_for_event(EventType.IMAP_MESSAGE_MOVED)
|
||||
ac2.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
|
||||
ac2_direct_imap.select_config_folder("inbox")
|
||||
nr_msgs = len(ac2_direct_imap.get_all_messages())
|
||||
assert nr_msgs > 0
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import EventType
|
||||
from deltachat_rpc_client.const import DownloadState
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
@@ -37,8 +38,8 @@ def test_add_second_address(acfactory) -> None:
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.set_config(option, "1")
|
||||
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.set_config("show_emails", "0")
|
||||
# show_emails does not matter for multi-relay, can be set to anything
|
||||
account.set_config("show_emails", "0")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", ["mvbox_move", "only_fetch_mvbox"])
|
||||
@@ -57,8 +58,8 @@ def test_no_second_transport_with_mvbox(acfactory, key) -> None:
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
|
||||
def test_no_second_transport_without_classic_emails(acfactory) -> None:
|
||||
"""Test that second transport cannot be configured if classic emails are not fetched."""
|
||||
def test_second_transport_without_classic_emails(acfactory) -> None:
|
||||
"""Test that second transport can be configured if classic emails are not fetched."""
|
||||
account = acfactory.new_configured_account()
|
||||
assert len(account.list_transports()) == 1
|
||||
|
||||
@@ -67,8 +68,7 @@ def test_no_second_transport_without_classic_emails(acfactory) -> None:
|
||||
qr = acfactory.get_account_qr()
|
||||
account.set_config("show_emails", "0")
|
||||
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.add_transport_from_qr(qr)
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
|
||||
def test_change_address(acfactory) -> None:
|
||||
@@ -120,6 +120,33 @@ def test_change_address(acfactory) -> None:
|
||||
assert sender_addr2 == new_alice_addr
|
||||
|
||||
|
||||
def test_download_on_demand(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice.set_config("download_limit", "1")
|
||||
|
||||
alice.stop_io()
|
||||
qr = acfactory.get_account_qr()
|
||||
alice.add_transport_from_qr(qr)
|
||||
alice.start_io()
|
||||
|
||||
alice.create_chat(bob)
|
||||
chat_bob_alice = bob.create_chat(alice)
|
||||
chat_bob_alice.send_message(file="../test-data/image/screenshot.jpg")
|
||||
msg = alice.wait_for_incoming_msg()
|
||||
snapshot = msg.get_snapshot()
|
||||
assert snapshot.download_state == DownloadState.AVAILABLE
|
||||
chat_id = snapshot.chat_id
|
||||
# Actually the message isn't available yet. Wait somehow for the post-message to arrive.
|
||||
chat_bob_alice.send_message("Now you can download my previous message")
|
||||
alice.wait_for_incoming_msg()
|
||||
alice._rpc.download_full_message(alice.id, msg.id)
|
||||
for dstate in [DownloadState.IN_PROGRESS, DownloadState.DONE]:
|
||||
event = alice.wait_for_event(EventType.MSGS_CHANGED)
|
||||
assert event.chat_id == chat_id
|
||||
assert event.msg_id == msg.id
|
||||
assert msg.get_snapshot().download_state == dstate
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_chatmail", ["0", "1"])
|
||||
def test_mvbox_move_first_transport(acfactory, is_chatmail) -> None:
|
||||
"""Test that mvbox_move is disabled by default even for non-chatmail accounts.
|
||||
@@ -207,6 +234,38 @@ def test_transport_synchronization(acfactory, log) -> None:
|
||||
assert ac1_clone.wait_for_incoming_msg().get_snapshot().text == "Hello!"
|
||||
|
||||
|
||||
def test_transport_sync_new_as_primary(acfactory, log) -> None:
|
||||
"""Test synchronization of new transport as primary between devices."""
|
||||
ac1, bob = acfactory.get_online_accounts(2)
|
||||
ac1_clone = ac1.clone()
|
||||
ac1_clone.bring_online()
|
||||
|
||||
qr = acfactory.get_account_qr()
|
||||
|
||||
ac1.add_transport_from_qr(qr)
|
||||
ac1_transports = ac1.list_transports()
|
||||
assert len(ac1_transports) == 2
|
||||
[transport1, transport2] = ac1_transports
|
||||
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
assert len(ac1_clone.list_transports()) == 2
|
||||
assert ac1_clone.get_config("configured_addr") == transport1["addr"]
|
||||
|
||||
log.section("ac1 changes the primary transport")
|
||||
ac1.set_config("configured_addr", transport2["addr"])
|
||||
|
||||
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
assert ac1_clone.get_config("configured_addr") == transport2["addr"]
|
||||
|
||||
log.section("ac1_clone receives a message via the new primary transport")
|
||||
ac1_chat = ac1.create_chat(bob)
|
||||
ac1_chat.send_text("Hello!")
|
||||
bob_chat_id = bob.wait_for_incoming_msg_event().chat_id
|
||||
bob_chat = bob.get_chat_by_id(bob_chat_id)
|
||||
bob_chat.accept()
|
||||
bob_chat.send_text("hello back")
|
||||
assert ac1_clone.wait_for_incoming_msg().get_snapshot().text == "hello back"
|
||||
|
||||
|
||||
def test_recognize_self_address(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -221,3 +280,58 @@ def test_recognize_self_address(acfactory) -> None:
|
||||
bob_chat.send_text("Hello!")
|
||||
msg = alice.wait_for_incoming_msg().get_snapshot()
|
||||
assert msg.chat == alice.create_chat(bob)
|
||||
|
||||
|
||||
def test_transport_limit(acfactory) -> None:
|
||||
"""Test transports limit."""
|
||||
account = acfactory.get_online_account()
|
||||
qr = acfactory.get_account_qr()
|
||||
|
||||
limit = 5
|
||||
|
||||
for _ in range(1, limit):
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
assert len(account.list_transports()) == limit
|
||||
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
second_addr = account.list_transports()[1]["addr"]
|
||||
account.delete_transport(second_addr)
|
||||
|
||||
# test that adding a transport after deleting one works again
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
|
||||
def test_message_info_imap_urls(acfactory, log) -> None:
|
||||
"""Test that message info contains IMAP URLs of where the message was received."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
log.section("Alice adds ac1 clone removes second transport")
|
||||
qr = acfactory.get_account_qr()
|
||||
for i in range(3):
|
||||
alice.add_transport_from_qr(qr)
|
||||
# Wait for all transports to go IDLE after adding each one.
|
||||
for _ in range(i + 1):
|
||||
alice.bring_online()
|
||||
|
||||
new_alice_addr = alice.list_transports()[2]["addr"]
|
||||
alice.set_config("configured_addr", new_alice_addr)
|
||||
|
||||
# Enable multi-device mode so messages are not deleted immediately.
|
||||
alice.set_config("bcc_self", "1")
|
||||
|
||||
# Bob creates chat, learning about Alice's currently selected transport.
|
||||
# This is where he will send the message.
|
||||
bob_chat = bob.create_chat(alice)
|
||||
|
||||
# Alice changes the transport again.
|
||||
alice.set_config("configured_addr", alice.list_transports()[3]["addr"])
|
||||
|
||||
bob_chat.send_text("Hello!")
|
||||
|
||||
msg = alice.wait_for_incoming_msg()
|
||||
for alice_transport in alice.list_transports():
|
||||
addr = alice_transport["addr"]
|
||||
assert (addr == new_alice_addr) == (addr in msg.get_info())
|
||||
|
||||
@@ -696,6 +696,6 @@ def test_withdraw_securejoin_qr(acfactory):
|
||||
event = alice.wait_for_event()
|
||||
if (
|
||||
event.kind == EventType.WARNING
|
||||
and "Ignoring vg-request-with-auth message because of invalid auth code." in event.msg
|
||||
and "Ignoring RequestWithAuth message because of invalid auth code." in event.msg
|
||||
):
|
||||
break
|
||||
|
||||
@@ -10,7 +10,7 @@ from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import Contact, EventType, Message, events
|
||||
from deltachat_rpc_client import EventType, events
|
||||
from deltachat_rpc_client.const import DownloadState, MessageState
|
||||
from deltachat_rpc_client.pytestplugin import E2EE_INFO_MSGS
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
@@ -90,12 +90,9 @@ def test_lowercase_address(acfactory) -> None:
|
||||
assert account.get_config("configured_addr") == addr
|
||||
assert account.list_transports()[0]["addr"] == addr
|
||||
|
||||
for param in [
|
||||
account.get_info()["used_account_settings"],
|
||||
account.get_info()["entered_account_settings"],
|
||||
]:
|
||||
assert addr in param
|
||||
assert addr_upper not in param
|
||||
param = account.get_info()["used_transport_settings"]
|
||||
assert addr in param
|
||||
assert addr_upper not in param
|
||||
|
||||
|
||||
def test_configure_ip(acfactory) -> None:
|
||||
@@ -336,26 +333,27 @@ def test_receive_imf_failure(acfactory) -> None:
|
||||
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")
|
||||
bob.set_config("simulate_receive_imf_error", "1")
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
event = bob.wait_for_event(EventType.MSGS_CHANGED)
|
||||
assert event.chat_id == bob.get_device_chat().id
|
||||
msg_id = event.msg_id
|
||||
message = bob.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
version = bob.get_info()["deltachat_core_version"]
|
||||
assert (
|
||||
snapshot.text == "❌ Failed to receive a message:"
|
||||
" Condition failed: `!context.get_config_bool(Config::FailOnReceivingFullMsg).await?`."
|
||||
" Condition failed: `!context.get_config_bool(Config::SimulateReceiveImfError).await?`."
|
||||
f" Core version {version}."
|
||||
" Please report this bug to delta@merlinux.eu or https://support.delta.chat/."
|
||||
)
|
||||
|
||||
# The failed message doesn't break the IMAP loop.
|
||||
bob.set_config("fail_on_receiving_full_msg", "0")
|
||||
bob.set_config("simulate_receive_imf_error", "0")
|
||||
alice_chat_bob.send_text("Hello again!")
|
||||
message = bob.wait_for_incoming_msg()
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.text == "Hello again!"
|
||||
assert snapshot.download_state == DownloadState.DONE
|
||||
assert snapshot.error is None
|
||||
|
||||
|
||||
@@ -373,17 +371,48 @@ def test_selfavatar_sync(acfactory, data, log) -> None:
|
||||
alice.set_config("selfavatar", image)
|
||||
avatar_config = alice.get_config("selfavatar")
|
||||
avatar_hash = os.path.basename(avatar_config)
|
||||
print("Info: avatar hash is ", avatar_hash)
|
||||
logging.info(f"Avatar hash is {avatar_hash}")
|
||||
|
||||
log.section("First device receives avatar change")
|
||||
alice2.wait_for_event(EventType.SELFAVATAR_CHANGED)
|
||||
avatar_config2 = alice2.get_config("selfavatar")
|
||||
avatar_hash2 = os.path.basename(avatar_config2)
|
||||
print("Info: avatar hash on second device is ", avatar_hash2)
|
||||
logging.info(f"Avatar hash on second device is {avatar_hash2}")
|
||||
assert avatar_hash == avatar_hash2
|
||||
assert avatar_config != avatar_config2
|
||||
|
||||
|
||||
def test_dont_move_sync_msgs(acfactory, direct_imap):
|
||||
addr, password = acfactory.get_credentials()
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
ac1.set_config("bcc_self", "1")
|
||||
ac1.set_config("fix_is_chatmail", "1")
|
||||
ac1.add_or_update_transport({"addr": addr, "password": password})
|
||||
ac1.start_io()
|
||||
ac1_direct_imap = direct_imap(ac1)
|
||||
|
||||
# Sync messages may also be sent during configuration.
|
||||
ac1.wait_for_event(EventType.MSG_DELIVERED)
|
||||
ac1_direct_imap.select_folder("Inbox")
|
||||
while True:
|
||||
if len(ac1_direct_imap.get_all_messages()) == 1:
|
||||
break
|
||||
time.sleep(1)
|
||||
|
||||
ac1.set_config("displayname", "Alice")
|
||||
ac1.wait_for_event(EventType.MSG_DELIVERED)
|
||||
ac1.set_config("displayname", "Bob")
|
||||
ac1.wait_for_event(EventType.MSG_DELIVERED)
|
||||
|
||||
# Message may not be delivered to IMAP immediately
|
||||
# after sending over SMTP,
|
||||
# retry until they are delivered to IMAP.
|
||||
while True:
|
||||
if len(ac1_direct_imap.get_all_messages()) == 3:
|
||||
break
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
def test_reaction_seen_on_another_dev(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice2 = alice.clone()
|
||||
@@ -508,6 +537,103 @@ def test_import_export_backup(acfactory, tmp_path) -> None:
|
||||
assert alice2.manager.get_system_info()
|
||||
|
||||
|
||||
def test_import_export_online_all(acfactory, tmp_path, data, log) -> None:
|
||||
(ac1, some1) = acfactory.get_online_accounts(2)
|
||||
|
||||
log.section("create some chat content")
|
||||
some1_addr = some1.get_config("addr")
|
||||
chat1 = ac1.create_contact(some1).create_chat()
|
||||
chat1.send_text("msg1")
|
||||
assert len(ac1.get_contacts()) == 1
|
||||
|
||||
original_image_path = data.get_path("image/avatar64x64.png")
|
||||
chat1.send_file(str(original_image_path))
|
||||
|
||||
# Add another 100KB file that ensures that the progress is smooth enough
|
||||
path = tmp_path / "attachment.txt"
|
||||
with path.open("w") as file:
|
||||
file.truncate(100000)
|
||||
chat1.send_file(str(path))
|
||||
|
||||
def assert_account_is_proper(ac):
|
||||
contacts = ac.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.get_snapshot().address == some1_addr
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 3 + E2EE_INFO_MSGS
|
||||
assert messages[0 + E2EE_INFO_MSGS].get_snapshot().text == "msg1"
|
||||
snapshot = messages[1 + E2EE_INFO_MSGS].get_snapshot()
|
||||
assert snapshot.file_mime == "image/png"
|
||||
assert os.stat(snapshot.file).st_size == os.stat(original_image_path).st_size
|
||||
ac.set_config("displayname", "new displayname")
|
||||
assert ac.get_config("displayname") == "new displayname"
|
||||
|
||||
assert_account_is_proper(ac1)
|
||||
|
||||
backupdir = tmp_path / "backup"
|
||||
backupdir.mkdir()
|
||||
|
||||
log.section(f"export all to {backupdir}")
|
||||
ac1.stop_io()
|
||||
ac1.export_backup(backupdir)
|
||||
progress = 0
|
||||
files_written = []
|
||||
while True:
|
||||
event = ac1.wait_for_event()
|
||||
if event.kind == EventType.IMEX_PROGRESS:
|
||||
assert event.progress > 0 # Progress 0 indicates error.
|
||||
assert event.progress < progress + 250
|
||||
progress = event.progress
|
||||
if progress == 1000:
|
||||
break
|
||||
elif event.kind == EventType.IMEX_FILE_WRITTEN:
|
||||
files_written.append(event.path)
|
||||
else:
|
||||
logging.info(event)
|
||||
assert len(files_written) == 1
|
||||
assert os.path.exists(files_written[0])
|
||||
ac1.start_io()
|
||||
|
||||
log.section("get fresh empty account")
|
||||
ac2 = acfactory.get_unconfigured_account()
|
||||
|
||||
log.section("import backup and check it's proper")
|
||||
ac2.import_backup(files_written[0])
|
||||
progress = 0
|
||||
while True:
|
||||
event = ac2.wait_for_event()
|
||||
if event.kind == EventType.IMEX_PROGRESS:
|
||||
assert event.progress > 0 # Progress 0 indicates error.
|
||||
assert event.progress < progress + 250
|
||||
progress = event.progress
|
||||
if progress == 1000:
|
||||
break
|
||||
else:
|
||||
logging.info(event)
|
||||
assert_account_is_proper(ac1)
|
||||
assert_account_is_proper(ac2)
|
||||
|
||||
log.section(f"Second-time export all to {backupdir}")
|
||||
ac1.stop_io()
|
||||
ac1.export_backup(backupdir)
|
||||
while True:
|
||||
event = ac1.wait_for_event()
|
||||
if event.kind == EventType.IMEX_PROGRESS:
|
||||
assert event.progress > 0
|
||||
if event.progress == 1000:
|
||||
break
|
||||
elif event.kind == EventType.IMEX_FILE_WRITTEN:
|
||||
files_written.append(event.path)
|
||||
else:
|
||||
logging.info(event)
|
||||
assert len(files_written) == 2
|
||||
assert os.path.exists(files_written[1])
|
||||
assert files_written[1] != files_written[0]
|
||||
assert len(list(backupdir.glob("*.tar"))) == 2
|
||||
|
||||
|
||||
def test_import_export_keys(acfactory, tmp_path) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -591,60 +717,6 @@ def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
|
||||
assert snapshot.show_padlock
|
||||
|
||||
|
||||
def test_reaction_to_partially_fetched_msg(acfactory, tmp_path):
|
||||
"""See https://github.com/deltachat/deltachat-core-rust/issues/3688 "Partially downloaded
|
||||
messages are received out of order".
|
||||
|
||||
If the Inbox contains X small messages followed by Y large messages followed by Z small
|
||||
messages, Delta Chat first downloaded a batch of X+Z messages, and then a batch of Y messages.
|
||||
|
||||
This bug was discovered by @Simon-Laux while testing reactions PR #3644 and can be reproduced
|
||||
with online test as follows:
|
||||
- Bob enables download limit and goes offline.
|
||||
- Alice sends a large message to Bob and reacts to this message with a thumbs-up.
|
||||
- Bob goes online
|
||||
- Bob first processes a reaction message and throws it away because there is no corresponding
|
||||
message, then processes a partially downloaded message.
|
||||
- As a result, Bob does not see a reaction
|
||||
"""
|
||||
download_limit = 300000
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1_addr = ac1.get_config("addr")
|
||||
chat = ac1.create_chat(ac2)
|
||||
ac2.set_config("download_limit", str(download_limit))
|
||||
ac2.stop_io()
|
||||
|
||||
logging.info("sending small+large messages from ac1 to ac2")
|
||||
msgs = []
|
||||
msgs.append(chat.send_text("hi"))
|
||||
path = tmp_path / "large"
|
||||
path.write_bytes(os.urandom(download_limit + 1))
|
||||
msgs.append(chat.send_file(str(path)))
|
||||
for m in msgs:
|
||||
m.wait_until_delivered()
|
||||
|
||||
logging.info("sending a reaction to the large message from ac1 to ac2")
|
||||
# TODO: Find the reason of an occasional message reordering on the server (so that the reaction
|
||||
# has a lower UID than the previous message). W/a is to sleep for some time to let the reaction
|
||||
# have a later INTERNALDATE.
|
||||
time.sleep(1.1)
|
||||
react_str = "\N{THUMBS UP SIGN}"
|
||||
msgs.append(msgs[-1].send_reaction(react_str))
|
||||
msgs[-1].wait_until_delivered()
|
||||
|
||||
ac2.start_io()
|
||||
|
||||
logging.info("wait for ac2 to receive a reaction")
|
||||
msg2 = Message(ac2, ac2.wait_for_reactions_changed().msg_id)
|
||||
assert msg2.get_sender_contact().get_snapshot().address == ac1_addr
|
||||
assert msg2.get_snapshot().download_state == DownloadState.AVAILABLE
|
||||
reactions = msg2.get_reactions()
|
||||
contacts = [Contact(ac2, int(i)) for i in reactions.reactions_by_contact]
|
||||
assert len(contacts) == 1
|
||||
assert contacts[0].get_snapshot().address == ac1_addr
|
||||
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("n_accounts", [3, 2])
|
||||
def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
|
||||
download_limit = 300000
|
||||
@@ -671,14 +743,159 @@ def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
|
||||
path = tmp_path / "large"
|
||||
path.write_bytes(os.urandom(download_limit + 1))
|
||||
|
||||
n_done = 0
|
||||
for i in range(10):
|
||||
logging.info("Sending message %s", i)
|
||||
alice_group.send_file(str(path))
|
||||
snapshot = bob.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.download_state == DownloadState.AVAILABLE
|
||||
if snapshot.download_state == DownloadState.DONE:
|
||||
n_done += 1
|
||||
# Work around lost and reordered pre-messages.
|
||||
assert n_done <= 1
|
||||
else:
|
||||
assert snapshot.download_state == DownloadState.AVAILABLE
|
||||
assert snapshot.chat == bob_group
|
||||
|
||||
|
||||
def test_download_small_msg_first(acfactory, tmp_path):
|
||||
download_limit = 70000
|
||||
|
||||
alice, bob0 = acfactory.get_online_accounts(2)
|
||||
bob1 = bob0.clone()
|
||||
bob1.set_config("download_limit", str(download_limit))
|
||||
|
||||
chat = alice.create_chat(bob0)
|
||||
path = tmp_path / "large_enough"
|
||||
path.write_bytes(os.urandom(download_limit + 1))
|
||||
# Less than 140K, so sent w/o a pre-message.
|
||||
chat.send_file(str(path))
|
||||
chat.send_text("hi")
|
||||
bob0.create_chat(alice)
|
||||
assert bob0.wait_for_incoming_msg().get_snapshot().text == ""
|
||||
assert bob0.wait_for_incoming_msg().get_snapshot().text == "hi"
|
||||
|
||||
bob1.start_io()
|
||||
bob1.create_chat(alice)
|
||||
assert bob1.wait_for_incoming_msg().get_snapshot().text == "hi"
|
||||
assert bob1.wait_for_incoming_msg().get_snapshot().text == ""
|
||||
|
||||
|
||||
@pytest.mark.parametrize("delete_chat", [False, True])
|
||||
def test_delete_available_msg(acfactory, tmp_path, direct_imap, delete_chat):
|
||||
"""
|
||||
Tests `DownloadState.AVAILABLE` message deletion on the receiver side.
|
||||
Also tests pre- and post-message deletion on the sender side.
|
||||
"""
|
||||
# Min. UI setting as of v2.35
|
||||
download_limit = 163840
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
bob.set_config("download_limit", str(download_limit))
|
||||
# Avoid immediate deletion from the server
|
||||
alice.set_config("bcc_self", "1")
|
||||
bob.set_config("bcc_self", "1")
|
||||
|
||||
chat_alice = alice.create_chat(bob)
|
||||
path = tmp_path / "large"
|
||||
path.write_bytes(os.urandom(download_limit + 1))
|
||||
msg_alice = chat_alice.send_file(str(path))
|
||||
msg_bob = bob.wait_for_incoming_msg()
|
||||
msg_bob_snapshot = msg_bob.get_snapshot()
|
||||
assert msg_bob_snapshot.download_state == DownloadState.AVAILABLE
|
||||
chat_bob = bob.get_chat_by_id(msg_bob_snapshot.chat_id)
|
||||
|
||||
# Avoid DeleteMessages sync message
|
||||
bob.set_config("bcc_self", "0")
|
||||
if delete_chat:
|
||||
chat_bob.delete()
|
||||
else:
|
||||
bob.delete_messages([msg_bob])
|
||||
alice.wait_for_event(EventType.SMTP_MESSAGE_SENT)
|
||||
alice.wait_for_event(EventType.SMTP_MESSAGE_SENT)
|
||||
alice.set_config("bcc_self", "0")
|
||||
if delete_chat:
|
||||
chat_alice.delete()
|
||||
else:
|
||||
alice.delete_messages([msg_alice])
|
||||
for acc in [bob, alice]:
|
||||
if not delete_chat:
|
||||
acc.wait_for_event(EventType.MSG_DELETED)
|
||||
acc_direct_imap = direct_imap(acc)
|
||||
# Messages may be deleted separately
|
||||
while True:
|
||||
acc.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
|
||||
while True:
|
||||
event = acc.wait_for_event()
|
||||
if event.kind == EventType.INFO and "Close/expunge succeeded." in event.msg:
|
||||
break
|
||||
if len(acc_direct_imap.get_all_messages()) == 0:
|
||||
break
|
||||
|
||||
|
||||
def test_delete_fully_downloaded_msg(acfactory, tmp_path, direct_imap):
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
# Avoid immediate deletion from the server
|
||||
bob.set_config("bcc_self", "1")
|
||||
|
||||
chat_alice = alice.create_chat(bob)
|
||||
path = tmp_path / "large"
|
||||
# Big enough to be sent with a pre-message
|
||||
path.write_bytes(os.urandom(300000))
|
||||
chat_alice.send_file(str(path))
|
||||
|
||||
msg = bob.wait_for_incoming_msg()
|
||||
msg_snapshot = msg.get_snapshot()
|
||||
assert msg_snapshot.download_state == DownloadState.AVAILABLE
|
||||
msgs_changed_event = bob.wait_for_msgs_changed_event()
|
||||
assert msgs_changed_event.msg_id == msg.id
|
||||
msg_snapshot = msg.get_snapshot()
|
||||
assert msg_snapshot.download_state == DownloadState.DONE
|
||||
|
||||
bob_direct_imap = direct_imap(bob)
|
||||
assert len(bob_direct_imap.get_all_messages()) == 2
|
||||
# Avoid DeleteMessages sync message
|
||||
bob.set_config("bcc_self", "0")
|
||||
bob.delete_messages([msg])
|
||||
bob.wait_for_event(EventType.MSG_DELETED)
|
||||
# Messages may be deleted separately
|
||||
while True:
|
||||
bob.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
|
||||
while True:
|
||||
event = bob.wait_for_event()
|
||||
if event.kind == EventType.INFO and "Close/expunge succeeded." in event.msg:
|
||||
break
|
||||
if len(bob_direct_imap.get_all_messages()) == 0:
|
||||
break
|
||||
|
||||
|
||||
def test_imap_autodelete_fully_downloaded_msg(acfactory, tmp_path, direct_imap):
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
chat_alice = alice.create_chat(bob)
|
||||
path = tmp_path / "large"
|
||||
# Big enough to be sent with a pre-message
|
||||
path.write_bytes(os.urandom(300000))
|
||||
chat_alice.send_file(str(path))
|
||||
|
||||
msg = bob.wait_for_incoming_msg()
|
||||
msg_snapshot = msg.get_snapshot()
|
||||
assert msg_snapshot.download_state == DownloadState.AVAILABLE
|
||||
msgs_changed_event = bob.wait_for_msgs_changed_event()
|
||||
assert msgs_changed_event.msg_id == msg.id
|
||||
msg_snapshot = msg.get_snapshot()
|
||||
assert msg_snapshot.download_state == DownloadState.DONE
|
||||
|
||||
bob_direct_imap = direct_imap(bob)
|
||||
# Messages may be deleted separately
|
||||
while True:
|
||||
if len(bob_direct_imap.get_all_messages()) == 0:
|
||||
break
|
||||
bob.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
|
||||
while True:
|
||||
event = bob.wait_for_event()
|
||||
if event.kind == EventType.INFO and "Close/expunge succeeded." in event.msg:
|
||||
break
|
||||
|
||||
|
||||
def test_markseen_contact_request(acfactory):
|
||||
"""
|
||||
Test that seen status is synchronized for contact request messages
|
||||
@@ -702,6 +919,47 @@ def test_markseen_contact_request(acfactory):
|
||||
assert message2.get_snapshot().state == MessageState.IN_SEEN
|
||||
|
||||
|
||||
@pytest.mark.parametrize("team_profile", [True, False])
|
||||
def test_no_markseen_in_team_profile(team_profile, acfactory):
|
||||
"""
|
||||
Test that seen status is synchronized iff `team_profile` isn't set.
|
||||
"""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
if team_profile:
|
||||
bob.set_config("team_profile", "1")
|
||||
|
||||
# Bob sets up a second device.
|
||||
bob2 = bob.clone()
|
||||
bob2.start_io()
|
||||
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
bob_chat_alice = bob.create_chat(alice)
|
||||
bob2.create_chat(alice)
|
||||
alice_chat_bob.send_text("Hello Bob!")
|
||||
|
||||
message = bob.wait_for_incoming_msg()
|
||||
message2 = bob2.wait_for_incoming_msg()
|
||||
assert message2.get_snapshot().state == MessageState.IN_FRESH
|
||||
|
||||
message.mark_seen()
|
||||
|
||||
# Send a message and wait until it arrives
|
||||
# in order to wait until Bob2 gets the markseen message.
|
||||
# This also tests that outgoing messages
|
||||
# don't mark preceeding messages as seen in team profiles.
|
||||
bob_chat_alice.send_text("Outgoing message")
|
||||
while True:
|
||||
outgoing = bob2.wait_for_msg(EventType.MSGS_CHANGED)
|
||||
if outgoing.id != 0:
|
||||
break
|
||||
assert outgoing.get_snapshot().text == "Outgoing message"
|
||||
|
||||
if team_profile:
|
||||
assert message2.get_snapshot().state == MessageState.IN_FRESH
|
||||
else:
|
||||
assert message2.get_snapshot().state == MessageState.IN_SEEN
|
||||
|
||||
|
||||
def test_read_receipt(acfactory):
|
||||
"""
|
||||
Test sending a read receipt and ensure it is attributed to the correct contact.
|
||||
@@ -721,6 +979,9 @@ def test_read_receipt(acfactory):
|
||||
assert len(read_receipts) == 1
|
||||
assert read_receipts[0].contact_id == alice_contact_bob.id
|
||||
|
||||
read_receipt_cnt = read_msg.get_read_receipt_count()
|
||||
assert read_receipt_cnt == 1
|
||||
|
||||
|
||||
def test_get_http_response(acfactory):
|
||||
alice = acfactory.new_configured_account()
|
||||
@@ -733,7 +994,7 @@ def test_configured_imap_certificate_checks(acfactory):
|
||||
alice = acfactory.new_configured_account()
|
||||
|
||||
# Certificate checks should be configured (not None)
|
||||
assert "cert_strict" in alice.get_info().used_account_settings
|
||||
assert "cert_strict" in alice.get_info().used_transport_settings
|
||||
|
||||
# "cert_old_automatic" is the value old Delta Chat core versions used
|
||||
# to mean user entered "imap_certificate_checks=0" (Automatic)
|
||||
@@ -746,7 +1007,7 @@ def test_configured_imap_certificate_checks(acfactory):
|
||||
#
|
||||
# Core 1.142.4, 1.142.5 and 1.142.6 saved this value due to bug.
|
||||
# This test is a regression test to prevent this happening again.
|
||||
assert "cert_old_automatic" not in alice.get_info().used_account_settings
|
||||
assert "cert_old_automatic" not in alice.get_info().used_transport_settings
|
||||
|
||||
|
||||
def test_no_old_msg_is_fresh(acfactory):
|
||||
@@ -924,6 +1185,30 @@ def test_leave_broadcast(acfactory, all_devices_online):
|
||||
check_account(bob2, bob2.create_contact(alice), inviter_side=False)
|
||||
|
||||
|
||||
def test_leave_and_delete_group(acfactory, log):
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
log.section("Alice creates a group")
|
||||
alice_chat = alice.create_group("Group")
|
||||
alice_chat.add_contact(bob)
|
||||
assert len(alice_chat.get_contacts()) == 2 # Alice and Bob
|
||||
alice_chat.send_text("hello")
|
||||
|
||||
log.section("Bob sees the group, and leaves and deletes it")
|
||||
msg = bob.wait_for_incoming_msg().get_snapshot()
|
||||
assert msg.text == "hello"
|
||||
msg.chat.accept()
|
||||
|
||||
msg.chat.leave()
|
||||
# Bob deletes the chat. This must not prevent the leave message from being sent.
|
||||
msg.chat.delete()
|
||||
|
||||
log.section("Alice receives the delete message")
|
||||
# After Bob left, only Alice will be left in the group:
|
||||
while len(alice_chat.get_contacts()) != 1:
|
||||
alice.wait_for_event(EventType.CHAT_MODIFIED)
|
||||
|
||||
|
||||
def test_immediate_autodelete(acfactory, direct_imap, log):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -1056,3 +1341,23 @@ def test_synchronize_member_list_on_group_rejoin(acfactory, log):
|
||||
|
||||
assert chat.num_contacts() == 2
|
||||
assert msg.get_snapshot().chat.num_contacts() == 2
|
||||
|
||||
|
||||
def test_large_message(acfactory) -> None:
|
||||
"""
|
||||
Test sending large message without download limit set,
|
||||
so it is sent with pre-message but downloaded without user interaction.
|
||||
"""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
alice_chat_bob.send_message(
|
||||
"Hello World, this message is bigger than 5 bytes",
|
||||
file="../test-data/image/screenshot.jpg",
|
||||
)
|
||||
|
||||
msg = bob.wait_for_incoming_msg()
|
||||
msgs_changed_event = bob.wait_for_msgs_changed_event()
|
||||
assert msg.id == msgs_changed_event.msg_id
|
||||
snapshot = msg.get_snapshot()
|
||||
assert snapshot.text == "Hello World, this message is bigger than 5 bytes"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.34.0"
|
||||
version = "2.41.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.34.0"
|
||||
"version": "2.41.0"
|
||||
}
|
||||
|
||||
@@ -24,6 +24,14 @@ use yerpc::{RpcClient, RpcSession};
|
||||
|
||||
#[tokio::main(flavor = "multi_thread")]
|
||||
async fn main() {
|
||||
// Logs from `log` crate and traces from `tracing` crate
|
||||
// are configurable with `RUST_LOG` environment variable
|
||||
// and go to stderr to avoid interfering with JSON-RPC using stdout.
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(EnvFilter::from_default_env())
|
||||
.with_writer(std::io::stderr)
|
||||
.init();
|
||||
|
||||
let r = main_impl().await;
|
||||
// From tokio documentation:
|
||||
// "For technical reasons, stdin is implemented by using an ordinary blocking read on a separate
|
||||
@@ -43,7 +51,7 @@ async fn main_impl() -> Result<()> {
|
||||
if let Some(arg) = args.next() {
|
||||
return Err(anyhow!("Unrecognized argument {arg:?}"));
|
||||
}
|
||||
eprintln!("{}", &*DC_VERSION_STR);
|
||||
eprintln!("{DC_VERSION_STR}");
|
||||
return Ok(());
|
||||
} else if first_arg.to_str() == Some("--openrpc") {
|
||||
if let Some(arg) = args.next() {
|
||||
@@ -64,14 +72,6 @@ async fn main_impl() -> Result<()> {
|
||||
#[cfg(target_family = "unix")]
|
||||
let mut sigterm = signal_unix::signal(signal_unix::SignalKind::terminate())?;
|
||||
|
||||
// Logs from `log` crate and traces from `tracing` crate
|
||||
// are configurable with `RUST_LOG` environment variable
|
||||
// and go to stderr to avoid interfering with JSON-RPC using stdout.
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(EnvFilter::from_default_env())
|
||||
.with_writer(std::io::stderr)
|
||||
.init();
|
||||
|
||||
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "accounts".to_string());
|
||||
log::info!("Starting with accounts directory `{path}`.");
|
||||
let writable = true;
|
||||
|
||||
@@ -16,7 +16,7 @@ ignore = [
|
||||
# Unmaintained rustls-pemfile
|
||||
# It is a transitive dependency of iroh 0.35.0,
|
||||
# this should be fixed by upgrading to iroh 1.0 once it is released.
|
||||
"RUSTSEC-2025-0134"
|
||||
"RUSTSEC-2025-0134",
|
||||
]
|
||||
|
||||
[bans]
|
||||
@@ -31,11 +31,10 @@ skip = [
|
||||
{ name = "derive_more", version = "1.0.0" },
|
||||
{ name = "event-listener", version = "2.5.3" },
|
||||
{ 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 = "lru", version = "0.12.3" },
|
||||
{ name = "lru", version = "0.12.5" },
|
||||
{ name = "netlink-packet-route", version = "0.17.1" },
|
||||
{ name = "nom", version = "7.1.3" },
|
||||
{ name = "rand_chacha", version = "0.3.1" },
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "2.34.0"
|
||||
version = "2.41.0"
|
||||
license = "MPL-2.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import os
|
||||
import queue
|
||||
import sys
|
||||
import base64
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
@@ -9,7 +8,6 @@ from imap_tools import AND
|
||||
|
||||
import deltachat as dc
|
||||
from deltachat import account_hookimpl, Message
|
||||
from deltachat.tracker import ImexTracker
|
||||
from deltachat.testplugin import E2EE_INFO_MSGS
|
||||
|
||||
|
||||
@@ -222,71 +220,6 @@ def test_webxdc_huge_update(acfactory, data, lp):
|
||||
assert update["payload"] == payload
|
||||
|
||||
|
||||
def test_webxdc_download_on_demand(acfactory, data, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
acfactory.introduce_each_other([ac1, ac2])
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
msg1 = Message.new_empty(ac1, "webxdc")
|
||||
msg1.set_text("message1")
|
||||
msg1.set_file(data.get_path("webxdc/minimal.xdc"))
|
||||
msg1 = chat.send_msg(msg1)
|
||||
assert msg1.is_webxdc()
|
||||
assert msg1.filename
|
||||
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.is_webxdc()
|
||||
|
||||
lp.sec("ac2 sets download limit")
|
||||
ac2.set_config("download_limit", "100")
|
||||
assert msg1.send_status_update({"payload": base64.b64encode(os.urandom(300000))}, "some test data")
|
||||
ac2_update = ac2._evtracker.wait_next_incoming_message()
|
||||
assert ac2_update.download_state == dc.const.DC_DOWNLOAD_AVAILABLE
|
||||
assert not msg2.get_status_updates()
|
||||
|
||||
ac2_update.download_full()
|
||||
ac2._evtracker.get_matching("DC_EVENT_WEBXDC_STATUS_UPDATE")
|
||||
assert msg2.get_status_updates()
|
||||
|
||||
# Get a event notifying that the message disappeared from the chat.
|
||||
msgs_changed_event = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
assert msgs_changed_event.data1 == msg2.chat.id
|
||||
assert msgs_changed_event.data2 == 0
|
||||
|
||||
|
||||
def test_enable_mvbox_move(acfactory, lp):
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
|
||||
lp.sec("ac2: start without mvbox thread")
|
||||
ac2 = acfactory.new_online_configuring_account(mvbox_move=False)
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
lp.sec("ac2: configuring mvbox")
|
||||
ac2.set_config("mvbox_move", "1")
|
||||
|
||||
lp.sec("ac1: send message and wait for ac2 to receive it")
|
||||
acfactory.get_accepted_chat(ac1, ac2).send_text("message1")
|
||||
assert ac2._evtracker.wait_next_incoming_message().text == "message1"
|
||||
|
||||
|
||||
def test_move_sync_msgs(acfactory):
|
||||
ac1 = acfactory.new_online_configuring_account(bcc_self=True, sync_msgs=True, fix_is_chatmail=True)
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
ac1.direct_imap.select_folder("DeltaChat")
|
||||
# Sync messages may also be sent during the configuration.
|
||||
mvbox_msg_cnt = len(ac1.direct_imap.get_all_messages())
|
||||
|
||||
ac1.set_config("displayname", "Alice")
|
||||
ac1._evtracker.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||
ac1.set_config("displayname", "Bob")
|
||||
ac1._evtracker.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||
ac1.direct_imap.select_folder("Inbox")
|
||||
assert len(ac1.direct_imap.get_all_messages()) == 0
|
||||
ac1.direct_imap.select_folder("DeltaChat")
|
||||
assert len(ac1.direct_imap.get_all_messages()) == mvbox_msg_cnt + 2
|
||||
|
||||
|
||||
def test_forward_messages(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = ac1.create_chat(ac2)
|
||||
@@ -402,7 +335,7 @@ def test_long_group_name(acfactory, lp):
|
||||
|
||||
|
||||
def test_send_self_message(acfactory, lp):
|
||||
ac1 = acfactory.new_online_configuring_account(mvbox_move=True, bcc_self=True)
|
||||
ac1 = acfactory.new_online_configuring_account(bcc_self=True)
|
||||
acfactory.bring_accounts_online()
|
||||
lp.sec("ac1: create self chat")
|
||||
chat = ac1.get_self_contact().create_chat()
|
||||
@@ -561,7 +494,7 @@ def test_reply_privately(acfactory):
|
||||
|
||||
|
||||
def test_mdn_asymmetric(acfactory, lp):
|
||||
ac1 = acfactory.new_online_configuring_account(mvbox_move=True)
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac2 = acfactory.new_online_configuring_account()
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
@@ -590,20 +523,14 @@ def test_mdn_asymmetric(acfactory, lp):
|
||||
ac2.mark_seen_messages([msg])
|
||||
|
||||
lp.sec("ac1: waiting for incoming activity")
|
||||
# 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 + 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.")
|
||||
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
|
||||
|
||||
# MDN is received even though MDNs are already disabled
|
||||
assert msg_out.is_out_mdn_received()
|
||||
|
||||
ac1.direct_imap.select_config_folder("mvbox")
|
||||
assert len(list(ac1.direct_imap.conn.fetch(AND(seen=True)))) == 1
|
||||
|
||||
|
||||
def test_send_receive_encrypt(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
@@ -826,86 +753,6 @@ def test_send_and_receive_image(acfactory, lp, data):
|
||||
assert m == msg_in
|
||||
|
||||
|
||||
def test_import_export_online_all(acfactory, tmp_path, data, lp):
|
||||
(ac1, some1) = acfactory.get_online_accounts(2)
|
||||
|
||||
lp.sec("create some chat content")
|
||||
some1_addr = some1.get_config("addr")
|
||||
chat1 = ac1.create_contact(some1).create_chat()
|
||||
chat1.send_text("msg1")
|
||||
assert len(ac1.get_contacts()) == 1
|
||||
|
||||
original_image_path = data.get_path("d.png")
|
||||
chat1.send_image(original_image_path)
|
||||
|
||||
# Add another 100KB file that ensures that the progress is smooth enough
|
||||
path = tmp_path / "attachment.txt"
|
||||
with path.open("w") as file:
|
||||
file.truncate(100000)
|
||||
chat1.send_file(str(path))
|
||||
|
||||
def assert_account_is_proper(ac):
|
||||
contacts = ac.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == some1_addr
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
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"
|
||||
|
||||
assert_account_is_proper(ac1)
|
||||
|
||||
backupdir = tmp_path / "backup"
|
||||
backupdir.mkdir()
|
||||
|
||||
lp.sec(f"export all to {backupdir}")
|
||||
with ac1.temp_plugin(ImexTracker()) as imex_tracker:
|
||||
ac1.stop_io()
|
||||
ac1.imex(str(backupdir), dc.const.DC_IMEX_EXPORT_BACKUP)
|
||||
|
||||
# check progress events for export
|
||||
assert imex_tracker.wait_progress(1, progress_upper_limit=249)
|
||||
assert imex_tracker.wait_progress(250, progress_upper_limit=499)
|
||||
assert imex_tracker.wait_progress(500, progress_upper_limit=749)
|
||||
assert imex_tracker.wait_progress(750, progress_upper_limit=999)
|
||||
|
||||
paths = imex_tracker.wait_finish()
|
||||
assert len(paths) == 1
|
||||
path = paths[0]
|
||||
assert os.path.exists(path)
|
||||
ac1.start_io()
|
||||
|
||||
lp.sec("get fresh empty account")
|
||||
ac2 = acfactory.get_unconfigured_account()
|
||||
|
||||
lp.sec("get latest backup file")
|
||||
path2 = ac2.get_latest_backupfile(str(backupdir))
|
||||
assert path2 == path
|
||||
|
||||
lp.sec("import backup and check it's proper")
|
||||
with ac2.temp_plugin(ImexTracker()) as imex_tracker:
|
||||
ac2.import_all(path)
|
||||
|
||||
# check progress events for import
|
||||
assert imex_tracker.wait_progress(1, progress_upper_limit=249)
|
||||
assert imex_tracker.wait_progress(1000)
|
||||
|
||||
assert_account_is_proper(ac1)
|
||||
assert_account_is_proper(ac2)
|
||||
|
||||
lp.sec(f"Second-time export all to {backupdir}")
|
||||
ac1.stop_io()
|
||||
path2 = ac1.export_all(str(backupdir))
|
||||
assert os.path.exists(path2)
|
||||
assert path2 != path
|
||||
assert ac2.get_latest_backupfile(str(backupdir)) == path2
|
||||
|
||||
|
||||
def test_qr_email_capitalization(acfactory, lp):
|
||||
"""Regression test for a bug
|
||||
that resulted in failure to propagate verification
|
||||
@@ -1295,16 +1142,17 @@ def test_configure_error_msgs_invalid_server(acfactory):
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_CONFIGURE_PROGRESS")
|
||||
if ev.data1 == 0:
|
||||
break
|
||||
err_lower = ev.data2.lower()
|
||||
# Can't connect so it probably should say something about "internet"
|
||||
# again, should not repeat itself
|
||||
# If this fails then probably `e.msg.to_lowercase().contains("could not resolve")`
|
||||
# in configure.rs returned false because the error message was changed
|
||||
# (i.e. did not contain "could not resolve" anymore)
|
||||
assert (ev.data2.count("internet") + ev.data2.count("network")) == 1
|
||||
assert (err_lower.count("internet") + err_lower.count("network")) == 1
|
||||
# Should mention that it can't connect:
|
||||
assert ev.data2.count("connect") == 1
|
||||
assert err_lower.count("connect") == 1
|
||||
# The users do not know what "configuration" is
|
||||
assert "configuration" not in ev.data2.lower()
|
||||
assert "configuration" not in err_lower
|
||||
|
||||
|
||||
def test_status(acfactory):
|
||||
|
||||
@@ -258,9 +258,6 @@ class TestOfflineChat:
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(dc.const.DC_STR_FILE, "xyz %1$s")
|
||||
ac1._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(dc.const.DC_STR_CONTACT_NOT_VERIFIED, "xyz %2$s")
|
||||
ac1._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(500, "xyz %1$s")
|
||||
ac1._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
|
||||
@@ -1 +1 @@
|
||||
2025-12-11
|
||||
2026-02-06
|
||||
@@ -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.91.0
|
||||
RUST_VERSION=1.93.0
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# Update package cache without changing the lockfile.
|
||||
cargo update --dry-run
|
||||
|
||||
cargo deny --workspace --all-features check -D warnings
|
||||
cargo deny --workspace --all-features --locked check -D warnings
|
||||
|
||||
@@ -6,11 +6,11 @@ set -euo pipefail
|
||||
export TZ=UTC
|
||||
|
||||
# Provider database revision.
|
||||
REV=d041136c19a48b493823b46d472f12b9ee94ae80
|
||||
REV=996c4bc82be5a7404f70b185ff062da33bfa98d9
|
||||
|
||||
CORE_ROOT="$PWD"
|
||||
TMP="$(mktemp -d)"
|
||||
git clone --filter=blob:none https://github.com/deltachat/provider-db.git "$TMP"
|
||||
git clone --filter=blob:none https://github.com/chatmail/provider-db.git "$TMP"
|
||||
cd "$TMP"
|
||||
git checkout "$REV"
|
||||
DATE=$(git show -s --format=%cs)
|
||||
|
||||
@@ -60,8 +60,18 @@ impl Accounts {
|
||||
if writable && !dir.exists() {
|
||||
Accounts::create(&dir).await?;
|
||||
}
|
||||
let events = Events::new();
|
||||
Accounts::open(events, dir, writable).await
|
||||
}
|
||||
|
||||
Accounts::open(dir, writable).await
|
||||
/// Loads or creates an accounts folder at the given `dir`.
|
||||
/// Uses an existing events channel.
|
||||
pub async fn new_with_events(dir: PathBuf, writable: bool, events: Events) -> Result<Self> {
|
||||
if writable && !dir.exists() {
|
||||
Accounts::create(&dir).await?;
|
||||
}
|
||||
|
||||
Accounts::open(events, dir, writable).await
|
||||
}
|
||||
|
||||
/// Get the ID used to log events.
|
||||
@@ -85,14 +95,14 @@ impl Accounts {
|
||||
|
||||
/// Opens an existing accounts structure. Will error if the folder doesn't exist,
|
||||
/// no account exists and no config exists.
|
||||
async fn open(dir: PathBuf, writable: bool) -> Result<Self> {
|
||||
async fn open(events: Events, dir: PathBuf, writable: bool) -> Result<Self> {
|
||||
ensure!(dir.exists(), "directory does not exist");
|
||||
|
||||
let config_file = dir.join(CONFIG_NAME);
|
||||
ensure!(config_file.exists(), "{config_file:?} does not exist");
|
||||
|
||||
let config = Config::from_file(config_file, writable).await?;
|
||||
let events = Events::new();
|
||||
|
||||
let stockstrings = StockStrings::new();
|
||||
let push_subscriber = PushSubscriber::new();
|
||||
let accounts = config
|
||||
|
||||
48
src/blob.rs
48
src/blob.rs
@@ -1,6 +1,6 @@
|
||||
//! # Blob directory management.
|
||||
|
||||
use core::cmp::max;
|
||||
use std::cmp::max;
|
||||
use std::io::{Cursor, Seek};
|
||||
use std::iter::FusedIterator;
|
||||
use std::mem;
|
||||
@@ -256,7 +256,7 @@ impl<'a> BlobObject<'a> {
|
||||
|
||||
/// Recode image to avatar size.
|
||||
pub async fn recode_to_avatar_size(&mut self, context: &Context) -> Result<()> {
|
||||
let (img_wh, max_bytes) =
|
||||
let (max_wh, max_bytes) =
|
||||
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
@@ -273,7 +273,7 @@ impl<'a> BlobObject<'a> {
|
||||
let is_avatar = true;
|
||||
self.check_or_recode_to_size(
|
||||
context, None, // The name of an avatar doesn't matter
|
||||
viewtype, img_wh, max_bytes, is_avatar,
|
||||
viewtype, max_wh, max_bytes, is_avatar,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
@@ -294,7 +294,7 @@ impl<'a> BlobObject<'a> {
|
||||
name: Option<String>,
|
||||
viewtype: &mut Viewtype,
|
||||
) -> Result<String> {
|
||||
let (img_wh, max_bytes) =
|
||||
let (max_wh, max_bytes) =
|
||||
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
@@ -305,13 +305,15 @@ impl<'a> BlobObject<'a> {
|
||||
MediaQuality::Worse => (constants::WORSE_IMAGE_SIZE, constants::WORSE_IMAGE_BYTES),
|
||||
};
|
||||
let is_avatar = false;
|
||||
self.check_or_recode_to_size(context, name, viewtype, img_wh, max_bytes, is_avatar)
|
||||
self.check_or_recode_to_size(context, name, viewtype, max_wh, max_bytes, is_avatar)
|
||||
}
|
||||
|
||||
/// Checks or recodes the image so that it fits into limits on width/height and byte size.
|
||||
/// Checks or recodes the image so that it fits into limits on width/height and/or byte size.
|
||||
///
|
||||
/// If `!is_avatar`, then if `max_bytes` is exceeded, reduces the image to `img_wh` and proceeds
|
||||
/// with the result without rechecking.
|
||||
/// If `!is_avatar`, then if `max_bytes` is exceeded, reduces the image to `max_wh` and proceeds
|
||||
/// with the result (even if `max_bytes` is still exceeded).
|
||||
///
|
||||
/// If `is_avatar`, the resolution will be reduced in a loop until the image fits `max_bytes`.
|
||||
///
|
||||
/// This modifies the blob object in-place.
|
||||
///
|
||||
@@ -324,7 +326,7 @@ impl<'a> BlobObject<'a> {
|
||||
context: &Context,
|
||||
name: Option<String>,
|
||||
viewtype: &mut Viewtype,
|
||||
mut img_wh: u32,
|
||||
max_wh: u32,
|
||||
max_bytes: usize,
|
||||
is_avatar: bool,
|
||||
) -> Result<String> {
|
||||
@@ -386,7 +388,14 @@ impl<'a> BlobObject<'a> {
|
||||
_ => img,
|
||||
};
|
||||
|
||||
let exceeds_wh = img.width() > img_wh || img.height() > img_wh;
|
||||
// max_wh is the maximum image width and height, i.e. the resolution-limit.
|
||||
// target_wh target-resolution for resizing the image.
|
||||
let exceeds_wh = img.width() > max_wh || img.height() > max_wh;
|
||||
let mut target_wh = if exceeds_wh {
|
||||
max_wh
|
||||
} else {
|
||||
max(img.width(), img.height())
|
||||
};
|
||||
let exceeds_max_bytes = nr_bytes > max_bytes as u64;
|
||||
|
||||
let jpeg_quality = 75;
|
||||
@@ -425,15 +434,6 @@ impl<'a> BlobObject<'a> {
|
||||
});
|
||||
|
||||
if do_scale {
|
||||
if !exceeds_wh {
|
||||
img_wh = max(img.width(), img.height());
|
||||
// PNGs and WebPs may be huge because of animation, which is lost by the `image`
|
||||
// crate when recoding, so don't scale them down.
|
||||
if matches!(fmt, ImageFormat::Jpeg) || !encoded.is_empty() {
|
||||
img_wh = img_wh * 2 / 3;
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
if mem::take(&mut add_white_bg) {
|
||||
self::add_white_bg(&mut img);
|
||||
@@ -448,9 +448,9 @@ impl<'a> BlobObject<'a> {
|
||||
// usually has less pixels by cropping, UI that needs to wait anyways,
|
||||
// and also benefits from slightly better (5%) encoding of Triangle-filtered images.
|
||||
let new_img = if is_avatar {
|
||||
img.resize(img_wh, img_wh, image::imageops::FilterType::Triangle)
|
||||
img.resize(target_wh, target_wh, image::imageops::FilterType::Triangle)
|
||||
} else {
|
||||
img.thumbnail(img_wh, img_wh)
|
||||
img.thumbnail(target_wh, target_wh)
|
||||
};
|
||||
|
||||
if encoded_img_exceeds_bytes(
|
||||
@@ -461,19 +461,19 @@ impl<'a> BlobObject<'a> {
|
||||
&mut encoded,
|
||||
)? && is_avatar
|
||||
{
|
||||
if img_wh < 20 {
|
||||
if target_wh < 20 {
|
||||
return Err(format_err!(
|
||||
"Failed to scale image to below {max_bytes}B.",
|
||||
));
|
||||
}
|
||||
|
||||
img_wh = img_wh * 2 / 3;
|
||||
target_wh = target_wh * 2 / 3;
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"Final scaled-down image size: {}B ({}px).",
|
||||
encoded.len(),
|
||||
img_wh
|
||||
target_wh
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -798,3 +798,56 @@ async fn test_create_and_deduplicate_from_bytes() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that an image that already fits into the width limit,
|
||||
/// but not the bytes limit,
|
||||
/// is compressed without changing the resolution.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_without_downscaling() -> Result<()> {
|
||||
let t = &TestContext::new().await;
|
||||
|
||||
let image = include_bytes!("../../test-data/image/screenshot120x120.jpg");
|
||||
const { assert!(120 < constants::WORSE_AVATAR_SIZE) };
|
||||
|
||||
for is_avatar in [true, false] {
|
||||
let mut blob =
|
||||
BlobObject::create_and_deduplicate_from_bytes(t, image, "image.jpg").unwrap();
|
||||
let image_path = blob.to_abs_path();
|
||||
check_image_size(&image_path, 120, 120);
|
||||
|
||||
assert!(
|
||||
fs::metadata(&image_path).await.unwrap().len() > constants::WORSE_AVATAR_BYTES as u64
|
||||
);
|
||||
|
||||
// Repeat the check, because a second call to `check_or_recode_to_size()`
|
||||
// is not supposed to change anything:
|
||||
let mut imgs = vec![];
|
||||
for _ in 0..2 {
|
||||
let mut viewtype = Viewtype::Image;
|
||||
let new_name = blob.check_or_recode_to_size(
|
||||
t,
|
||||
Some("image.jpg".to_string()),
|
||||
&mut viewtype,
|
||||
constants::WORSE_AVATAR_SIZE,
|
||||
constants::WORSE_AVATAR_BYTES,
|
||||
is_avatar,
|
||||
)?;
|
||||
let image_path = blob.to_abs_path();
|
||||
assert_eq!(new_name, "image.jpg"); // The name shall not have changed
|
||||
assert_eq!(viewtype, Viewtype::Image); // The viewtype shall not have changed
|
||||
let img = check_image_size(&image_path, 120, 120); // The resolution shall not have changed
|
||||
imgs.push(img);
|
||||
|
||||
let new_image_bytes = fs::metadata(&image_path).await.unwrap().len();
|
||||
assert!(
|
||||
new_image_bytes < constants::WORSE_AVATAR_BYTES as u64,
|
||||
"The new image size, {new_image_bytes}, should be lower than {}, is_avatar={is_avatar}",
|
||||
constants::WORSE_AVATAR_BYTES
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(imgs[0], imgs[1]);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
313
src/calls.rs
313
src/calls.rs
@@ -4,6 +4,7 @@
|
||||
//! This means, the "Call ID" is a "Message ID" - similar to Webxdc IDs.
|
||||
use crate::chat::ChatIdBlocked;
|
||||
use crate::chat::{Chat, ChatId, send_msg};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Blocked, Chattype};
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::{Context, WeakContext};
|
||||
@@ -14,11 +15,12 @@ use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::net::dns::lookup_host_with_cache;
|
||||
use crate::param::Param;
|
||||
use crate::stock_str;
|
||||
use crate::tools::{normalize_text, time};
|
||||
use anyhow::{Context as _, Result, ensure};
|
||||
use sdp::SessionDescription;
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use num_traits::FromPrimitive;
|
||||
use serde::Serialize;
|
||||
use std::io::Cursor;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use tokio::task;
|
||||
@@ -101,10 +103,14 @@ impl CallInfo {
|
||||
};
|
||||
|
||||
if self.is_incoming() {
|
||||
self.update_text(context, &format!("Incoming call\n{duration}"))
|
||||
let incoming_call_str =
|
||||
stock_str::incoming_call(context, self.has_video_initially()).await;
|
||||
self.update_text(context, &format!("{incoming_call_str}\n{duration}"))
|
||||
.await?;
|
||||
} else {
|
||||
self.update_text(context, &format!("Outgoing call\n{duration}"))
|
||||
let outgoing_call_str =
|
||||
stock_str::outgoing_call(context, self.has_video_initially()).await;
|
||||
self.update_text(context, &format!("{outgoing_call_str}\n{duration}"))
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -123,6 +129,14 @@ impl CallInfo {
|
||||
self.msg.param.exists(CALL_ACCEPTED_TIMESTAMP)
|
||||
}
|
||||
|
||||
/// Returns true if the call is started as a video call.
|
||||
pub fn has_video_initially(&self) -> bool {
|
||||
self.msg
|
||||
.param
|
||||
.get_bool(Param::WebrtcHasVideoInitially)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Returns true if the call is missed
|
||||
/// because the caller canceled it
|
||||
/// explicitly before ringing stopped.
|
||||
@@ -182,6 +196,7 @@ impl Context {
|
||||
&self,
|
||||
chat_id: ChatId,
|
||||
place_call_info: String,
|
||||
has_video_initially: bool,
|
||||
) -> Result<MsgId> {
|
||||
let chat = Chat::load_from_db(self, chat_id).await?;
|
||||
ensure!(
|
||||
@@ -190,12 +205,15 @@ impl Context {
|
||||
);
|
||||
ensure!(!chat.is_self_talk(), "Cannot call self");
|
||||
|
||||
let outgoing_call_str = stock_str::outgoing_call(self, has_video_initially).await;
|
||||
let mut call = Message {
|
||||
viewtype: Viewtype::Call,
|
||||
text: "Outgoing call".into(),
|
||||
text: outgoing_call_str,
|
||||
..Default::default()
|
||||
};
|
||||
call.param.set(Param::WebrtcRoom, &place_call_info);
|
||||
call.param
|
||||
.set_int(Param::WebrtcHasVideoInitially, has_video_initially.into());
|
||||
call.id = send_msg(self, chat_id, &mut call).await?;
|
||||
|
||||
let wait = RINGING_SECONDS;
|
||||
@@ -263,10 +281,12 @@ impl Context {
|
||||
if !call.is_accepted() {
|
||||
if call.is_incoming() {
|
||||
call.mark_as_ended(self).await?;
|
||||
call.update_text(self, "Declined call").await?;
|
||||
let declined_call_str = stock_str::declined_call(self).await;
|
||||
call.update_text(self, &declined_call_str).await?;
|
||||
} else {
|
||||
call.mark_as_canceled(self).await?;
|
||||
call.update_text(self, "Canceled call").await?;
|
||||
let canceled_call_str = stock_str::canceled_call(self).await;
|
||||
call.update_text(self, &canceled_call_str).await?;
|
||||
}
|
||||
} else {
|
||||
call.mark_as_ended(self).await?;
|
||||
@@ -308,10 +328,12 @@ impl Context {
|
||||
if !call.is_accepted() && !call.is_ended() {
|
||||
if call.is_incoming() {
|
||||
call.mark_as_canceled(&context).await?;
|
||||
call.update_text(&context, "Missed call").await?;
|
||||
let missed_call_str = stock_str::missed_call(&context).await;
|
||||
call.update_text(&context, &missed_call_str).await?;
|
||||
} else {
|
||||
call.mark_as_ended(&context).await?;
|
||||
call.update_text(&context, "Canceled call").await?;
|
||||
let canceled_call_str = stock_str::canceled_call(&context).await;
|
||||
call.update_text(&context, &canceled_call_str).await?;
|
||||
}
|
||||
context.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
context.emit_event(EventType::CallEnded {
|
||||
@@ -336,38 +358,42 @@ impl Context {
|
||||
|
||||
if call.is_incoming() {
|
||||
if call.is_stale() {
|
||||
call.update_text(self, "Missed call").await?;
|
||||
let missed_call_str = stock_str::missed_call(self).await;
|
||||
call.update_text(self, &missed_call_str).await?;
|
||||
self.emit_incoming_msg(call.msg.chat_id, call_id); // notify missed call
|
||||
} else {
|
||||
call.update_text(self, "Incoming call").await?;
|
||||
let incoming_call_str =
|
||||
stock_str::incoming_call(self, call.has_video_initially()).await;
|
||||
call.update_text(self, &incoming_call_str).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
|
||||
}
|
||||
let can_call_me = match who_can_call_me(self).await? {
|
||||
WhoCanCallMe::Contacts => ChatIdBlocked::lookup_by_contact(self, from_id)
|
||||
.await?
|
||||
.is_some_and(|chat_id_blocked| {
|
||||
match chat_id_blocked.blocked {
|
||||
Blocked::Not => true,
|
||||
Blocked::Yes | Blocked::Request => {
|
||||
// Do not notify about incoming calls
|
||||
// from contact requests and blocked contacts.
|
||||
//
|
||||
// User can still access the call and accept it
|
||||
// via the chat in case of contact requests.
|
||||
false
|
||||
}
|
||||
}
|
||||
}),
|
||||
WhoCanCallMe::Everybody => ChatIdBlocked::lookup_by_contact(self, from_id)
|
||||
.await?
|
||||
.is_none_or(|chat_id_blocked| chat_id_blocked.blocked != Blocked::Yes),
|
||||
WhoCanCallMe::Nobody => false,
|
||||
};
|
||||
if let Some(chat_id_blocked) =
|
||||
ChatIdBlocked::lookup_by_contact(self, from_id).await?
|
||||
{
|
||||
match chat_id_blocked.blocked {
|
||||
Blocked::Not => {
|
||||
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,
|
||||
});
|
||||
}
|
||||
Blocked::Yes | Blocked::Request => {
|
||||
// Do not notify about incoming calls
|
||||
// from contact requests and blocked contacts.
|
||||
//
|
||||
// User can still access the call and accept it
|
||||
// via the chat in case of contact requests.
|
||||
}
|
||||
}
|
||||
if can_call_me {
|
||||
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: call.has_video_initially(),
|
||||
});
|
||||
}
|
||||
let wait = call.remaining_ring_seconds();
|
||||
let context = self.get_weak_context();
|
||||
@@ -378,7 +404,9 @@ impl Context {
|
||||
));
|
||||
}
|
||||
} else {
|
||||
call.update_text(self, "Outgoing call").await?;
|
||||
let outgoing_call_str =
|
||||
stock_str::outgoing_call(self, call.has_video_initially()).await;
|
||||
call.update_text(self, &outgoing_call_str).await?;
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
}
|
||||
} else {
|
||||
@@ -428,19 +456,23 @@ impl Context {
|
||||
if call.is_incoming() {
|
||||
if from_id == ContactId::SELF {
|
||||
call.mark_as_ended(self).await?;
|
||||
call.update_text(self, "Declined call").await?;
|
||||
let declined_call_str = stock_str::declined_call(self).await;
|
||||
call.update_text(self, &declined_call_str).await?;
|
||||
} else {
|
||||
call.mark_as_canceled(self).await?;
|
||||
call.update_text(self, "Missed call").await?;
|
||||
let missed_call_str = stock_str::missed_call(self).await;
|
||||
call.update_text(self, &missed_call_str).await?;
|
||||
}
|
||||
} else {
|
||||
// outgoing
|
||||
if from_id == ContactId::SELF {
|
||||
call.mark_as_canceled(self).await?;
|
||||
call.update_text(self, "Canceled call").await?;
|
||||
let canceled_call_str = stock_str::canceled_call(self).await;
|
||||
call.update_text(self, &canceled_call_str).await?;
|
||||
} else {
|
||||
call.mark_as_ended(self).await?;
|
||||
call.update_text(self, "Declined call").await?;
|
||||
let declined_call_str = stock_str::declined_call(self).await;
|
||||
call.update_text(self, &declined_call_str).await?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -496,19 +528,6 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@@ -606,33 +625,7 @@ struct IceServer {
|
||||
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.
|
||||
/// Creates ICE servers from a line received over IMAP METADATA.
|
||||
///
|
||||
/// IMAP METADATA returns a line such as
|
||||
/// `example.com:3478:1758650868:8Dqkyyu11MVESBqjbIylmB06rv8=`
|
||||
@@ -642,20 +635,107 @@ async fn create_ice_servers(
|
||||
/// while `8Dqkyyu11MVESBqjbIylmB06rv8=`
|
||||
/// is the password.
|
||||
pub(crate) async fn create_ice_servers_from_metadata(
|
||||
context: &Context,
|
||||
metadata: &str,
|
||||
) -> Result<(i64, String)> {
|
||||
) -> Result<(i64, Vec<UnresolvedIceServer>)> {
|
||||
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?;
|
||||
let ice_servers = vec![UnresolvedIceServer::Turn {
|
||||
hostname: hostname.to_string(),
|
||||
port,
|
||||
username: ts.to_string(),
|
||||
credential: password.to_string(),
|
||||
}];
|
||||
Ok((expiration_timestamp, ice_servers))
|
||||
}
|
||||
|
||||
/// STUN or TURN server with unresolved DNS name.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum UnresolvedIceServer {
|
||||
/// STUN server.
|
||||
Stun { hostname: String, port: u16 },
|
||||
|
||||
/// TURN server with the username and password.
|
||||
Turn {
|
||||
hostname: String,
|
||||
port: u16,
|
||||
username: String,
|
||||
credential: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Resolves domain names of ICE servers.
|
||||
///
|
||||
/// On failure to resolve, logs the error
|
||||
/// and skips the server, but does not fail.
|
||||
pub(crate) async fn resolve_ice_servers(
|
||||
context: &Context,
|
||||
unresolved_ice_servers: Vec<UnresolvedIceServer>,
|
||||
) -> Result<String> {
|
||||
let mut result: Vec<IceServer> = Vec::new();
|
||||
|
||||
// Do not use cache because there is no TLS.
|
||||
let load_cache = false;
|
||||
|
||||
for unresolved_ice_server in unresolved_ice_servers {
|
||||
match unresolved_ice_server {
|
||||
UnresolvedIceServer::Stun { hostname, port } => {
|
||||
match lookup_host_with_cache(context, &hostname, port, "", load_cache).await {
|
||||
Ok(addrs) => {
|
||||
let urls: Vec<String> = addrs
|
||||
.into_iter()
|
||||
.map(|addr| format!("stun:{addr}"))
|
||||
.collect();
|
||||
let stun_server = IceServer {
|
||||
urls,
|
||||
username: None,
|
||||
credential: None,
|
||||
};
|
||||
result.push(stun_server);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to resolve STUN {hostname}:{port}: {err:#}."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
UnresolvedIceServer::Turn {
|
||||
hostname,
|
||||
port,
|
||||
username,
|
||||
credential,
|
||||
} => match lookup_host_with_cache(context, &hostname, port, "", load_cache).await {
|
||||
Ok(addrs) => {
|
||||
let urls: Vec<String> = addrs
|
||||
.into_iter()
|
||||
.map(|addr| format!("turn:{addr}"))
|
||||
.collect();
|
||||
let turn_server = IceServer {
|
||||
urls,
|
||||
username: Some(username),
|
||||
credential: Some(credential),
|
||||
};
|
||||
result.push(turn_server);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to resolve TURN {hostname}:{port}: {err:#}."
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
let json = serde_json::to_string(&result)?;
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
/// Creates JSON with ICE servers when no TURN servers are known.
|
||||
pub(crate) async fn create_fallback_ice_servers(context: &Context) -> Result<String> {
|
||||
pub(crate) fn create_fallback_ice_servers() -> Vec<UnresolvedIceServer> {
|
||||
// Do not use public STUN server from https://stunprotocol.org/.
|
||||
// It changes the hostname every year
|
||||
// (e.g. stunserver2025.stunprotocol.org
|
||||
@@ -663,25 +743,18 @@ pub(crate) async fn create_fallback_ice_servers(context: &Context) -> Result<Str
|
||||
// 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)
|
||||
vec![
|
||||
UnresolvedIceServer::Stun {
|
||||
hostname: "nine.testrun.org".to_string(),
|
||||
port: STUN_PORT,
|
||||
},
|
||||
UnresolvedIceServer::Turn {
|
||||
hostname: "turn.delta.chat".to_string(),
|
||||
port: STUN_PORT,
|
||||
username: "public".to_string(),
|
||||
credential: "o4tR7yG4rG2slhXqRUf9zgmHz".to_string(),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/// Returns JSON with ICE servers.
|
||||
@@ -695,11 +768,39 @@ pub(crate) async fn create_fallback_ice_servers(context: &Context) -> Result<Str
|
||||
/// <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())
|
||||
let ice_servers = resolve_ice_servers(context, metadata.ice_servers.clone()).await?;
|
||||
Ok(ice_servers)
|
||||
} else {
|
||||
Ok("[]".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// "Who can call me" config options.
|
||||
#[derive(
|
||||
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||
)]
|
||||
#[repr(u8)]
|
||||
pub enum WhoCanCallMe {
|
||||
/// Everybody can call me if they are not blocked.
|
||||
///
|
||||
/// This includes contact requests.
|
||||
Everybody = 0,
|
||||
|
||||
/// Every contact who is not blocked and not a contact request, can call.
|
||||
#[default]
|
||||
Contacts = 1,
|
||||
|
||||
/// Nobody can call me.
|
||||
Nobody = 2,
|
||||
}
|
||||
|
||||
/// Returns currently configuration of the "who can call me" option.
|
||||
async fn who_can_call_me(context: &Context) -> Result<WhoCanCallMe> {
|
||||
let who_can_call_me =
|
||||
WhoCanCallMe::from_i32(context.get_config_int(Config::WhoCanCallMe).await?)
|
||||
.unwrap_or_default();
|
||||
Ok(who_can_call_me)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod calls_tests;
|
||||
|
||||
@@ -2,7 +2,7 @@ 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::receive_imf::receive_imf;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
|
||||
struct CallSetup {
|
||||
@@ -25,13 +25,6 @@ async fn assert_text(t: &TestContext, call_id: MsgId, text: &str) -> Result<()>
|
||||
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;
|
||||
@@ -52,7 +45,7 @@ async fn setup_call() -> Result<CallSetup> {
|
||||
bob2.create_chat(&alice).await;
|
||||
|
||||
let test_msg_id = alice
|
||||
.place_outgoing_call(alice_chat.id, PLACE_INFO.to_string())
|
||||
.place_outgoing_call(alice_chat.id, PLACE_INFO.to_string(), true)
|
||||
.await?;
|
||||
let sent1 = alice.pop_sent_msg().await;
|
||||
assert_eq!(sent1.sender_msg_id, test_msg_id);
|
||||
@@ -68,7 +61,8 @@ async fn setup_call() -> Result<CallSetup> {
|
||||
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!(info.has_video_initially(), true);
|
||||
assert_text(t, m.id, "Outgoing video call").await?;
|
||||
assert_eq!(call_state(t, m.id).await?, CallState::Alerting);
|
||||
}
|
||||
|
||||
@@ -89,7 +83,8 @@ async fn setup_call() -> Result<CallSetup> {
|
||||
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!(info.has_video_initially(), true);
|
||||
assert_text(t, m.id, "Incoming video call").await?;
|
||||
assert_eq!(call_state(t, m.id).await?, CallState::Alerting);
|
||||
}
|
||||
|
||||
@@ -120,7 +115,7 @@ async fn accept_call() -> Result<CallSetup> {
|
||||
// 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?;
|
||||
assert_text(&bob, bob_call.id, "Incoming video call").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
|
||||
.await;
|
||||
@@ -134,7 +129,7 @@ async fn accept_call() -> Result<CallSetup> {
|
||||
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?;
|
||||
assert_text(&bob, bob_call.id, "Incoming video call").await?;
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
|
||||
.await;
|
||||
@@ -147,7 +142,7 @@ async fn accept_call() -> Result<CallSetup> {
|
||||
|
||||
// Alice receives the acceptance message
|
||||
alice.recv_msg_trash(&sent2).await;
|
||||
assert_text(&alice, alice_call.id, "Outgoing call").await?;
|
||||
assert_text(&alice, alice_call.id, "Outgoing video call").await?;
|
||||
let ev = alice
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::OutgoingCallAccepted { .. }))
|
||||
@@ -169,7 +164,7 @@ async fn accept_call() -> Result<CallSetup> {
|
||||
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?;
|
||||
assert_text(&alice2, alice2_call.id, "Outgoing video call").await?;
|
||||
alice2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::OutgoingCallAccepted { .. }))
|
||||
@@ -208,7 +203,7 @@ async fn test_accept_call_callee_ends() -> Result<()> {
|
||||
|
||||
// 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?;
|
||||
assert_text(&bob, bob_call.id, "Incoming video call\n<1 minute").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
@@ -219,7 +214,7 @@ async fn test_accept_call_callee_ends() -> Result<()> {
|
||||
));
|
||||
|
||||
bob2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob2, bob2_call.id, "Incoming call\n<1 minute").await?;
|
||||
assert_text(&bob2, bob2_call.id, "Incoming video call\n<1 minute").await?;
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
@@ -230,7 +225,7 @@ async fn test_accept_call_callee_ends() -> Result<()> {
|
||||
|
||||
// Alice receives the ending message
|
||||
alice.recv_msg_trash(&sent3).await;
|
||||
assert_text(&alice, alice_call.id, "Outgoing call\n<1 minute").await?;
|
||||
assert_text(&alice, alice_call.id, "Outgoing video call\n<1 minute").await?;
|
||||
alice
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
@@ -241,7 +236,7 @@ async fn test_accept_call_callee_ends() -> Result<()> {
|
||||
));
|
||||
|
||||
alice2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&alice2, alice2_call.id, "Outgoing call\n<1 minute").await?;
|
||||
assert_text(&alice2, alice2_call.id, "Outgoing video call\n<1 minute").await?;
|
||||
alice2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
@@ -271,7 +266,7 @@ async fn test_accept_call_caller_ends() -> Result<()> {
|
||||
|
||||
// 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?;
|
||||
assert_text(&alice, alice_call.id, "Outgoing video call\n<1 minute").await?;
|
||||
alice
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
@@ -283,7 +278,7 @@ async fn test_accept_call_caller_ends() -> Result<()> {
|
||||
));
|
||||
|
||||
alice2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&alice2, alice2_call.id, "Outgoing call\n<1 minute").await?;
|
||||
assert_text(&alice2, alice2_call.id, "Outgoing video call\n<1 minute").await?;
|
||||
alice2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
@@ -295,7 +290,7 @@ async fn test_accept_call_caller_ends() -> Result<()> {
|
||||
|
||||
// Bob receives the ending message
|
||||
bob.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob, bob_call.id, "Incoming call\n<1 minute").await?;
|
||||
assert_text(&bob, bob_call.id, "Incoming video call\n<1 minute").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
@@ -305,7 +300,7 @@ async fn test_accept_call_caller_ends() -> Result<()> {
|
||||
));
|
||||
|
||||
bob2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob2, bob2_call.id, "Incoming call\n<1 minute").await?;
|
||||
assert_text(&bob2, bob2_call.id, "Incoming video call\n<1 minute").await?;
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
@@ -425,7 +420,7 @@ async fn test_caller_cancels_call() -> Result<()> {
|
||||
// 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");
|
||||
assert_eq!(summary.text, "🎥 Missed call");
|
||||
|
||||
bob2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob2, bob2_call.id, "Missed call").await?;
|
||||
@@ -525,13 +520,6 @@ async fn test_update_call_text() -> Result<()> {
|
||||
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<()> {
|
||||
@@ -542,7 +530,7 @@ async fn test_forward_call() -> Result<()> {
|
||||
|
||||
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())
|
||||
.place_outgoing_call(alice_bob_chat.id, PLACE_INFO.to_string(), true)
|
||||
.await
|
||||
.context("Failed to place a call")?;
|
||||
let alice_call = Message::load_from_db(alice, alice_msg_id).await?;
|
||||
@@ -610,65 +598,3 @@ async fn test_end_text_call() -> Result<()> {
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
314
src/chat.rs
314
src/chat.rs
@@ -12,12 +12,14 @@ use std::time::Duration;
|
||||
use anyhow::{Context as _, Result, anyhow, bail, ensure};
|
||||
use chrono::TimeZone;
|
||||
use deltachat_contact_tools::{ContactAddress, sanitize_bidi_characters, sanitize_single_line};
|
||||
use humansize::{BINARY, format_size};
|
||||
use mail_builder::mime::MimePart;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum_macros::EnumIter;
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::chatlist_events;
|
||||
use crate::color::str_to_color;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{
|
||||
@@ -27,7 +29,9 @@ use crate::constants::{
|
||||
use crate::contact::{self, Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::debug_logging::maybe_set_logging_xdc;
|
||||
use crate::download::DownloadState;
|
||||
use crate::download::{
|
||||
DownloadState, PRE_MSG_ATTACHMENT_SIZE_THRESHOLD, PRE_MSG_SIZE_WARNING_THRESHOLD,
|
||||
};
|
||||
use crate::ephemeral::{Timer as EphemeralTimer, start_chat_ephemeral_timers};
|
||||
use crate::events::EventType;
|
||||
use crate::key::self_fingerprint;
|
||||
@@ -35,11 +39,11 @@ use crate::location;
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::logged_debug_assert;
|
||||
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimefactory::MimeFactory;
|
||||
use crate::mimefactory::{MimeFactory, RenderedEmail};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::receive_imf::ReceivedMsg;
|
||||
use crate::smtp::send_msg_to_smtp;
|
||||
use crate::smtp::{self, send_msg_to_smtp};
|
||||
use crate::stock_str;
|
||||
use crate::sync::{self, Sync::*, SyncData};
|
||||
use crate::tools::{
|
||||
@@ -48,7 +52,6 @@ use crate::tools::{
|
||||
gm2local_offset, normalize_text, smeared_time, time, truncate_msg_text,
|
||||
};
|
||||
use crate::webxdc::StatusUpdateSerial;
|
||||
use crate::{chatlist_events, imap};
|
||||
|
||||
pub(crate) const PARAM_BROADCAST_SECRET: Param = Param::Arg3;
|
||||
|
||||
@@ -432,14 +435,18 @@ impl ChatId {
|
||||
|
||||
match chat.typ {
|
||||
Chattype::Single | Chattype::Group | Chattype::OutBroadcast | Chattype::InBroadcast => {
|
||||
// User has "created a chat" with all these contacts.
|
||||
//
|
||||
// Previously accepting a chat literally created a chat because unaccepted chats
|
||||
// went to "contact requests" list rather than normal chatlist.
|
||||
// But for groups we use lower origin because users don't always check all members
|
||||
// before accepting a chat and may not want to have the group members mixed with
|
||||
// existing contacts. `IncomingTo` fits here by its definition.
|
||||
let origin = match chat.typ {
|
||||
Chattype::Group => Origin::IncomingTo,
|
||||
_ => Origin::CreateChat,
|
||||
};
|
||||
for contact_id in get_chat_contacts(context, self).await? {
|
||||
if contact_id != ContactId::SELF {
|
||||
ContactId::scaleup_origin(context, &[contact_id], Origin::CreateChat)
|
||||
.await?;
|
||||
ContactId::scaleup_origin(context, &[contact_id], origin).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -596,6 +603,10 @@ impl ChatId {
|
||||
}
|
||||
|
||||
/// Deletes a chat.
|
||||
///
|
||||
/// Messages are deleted from the device and the chat database entry is deleted.
|
||||
/// After that, a `MsgsChanged` event is emitted.
|
||||
/// Messages are deleted from the server in background.
|
||||
pub async fn delete(self, context: &Context) -> Result<()> {
|
||||
self.delete_ex(context, Sync).await
|
||||
}
|
||||
@@ -607,7 +618,6 @@ impl ChatId {
|
||||
);
|
||||
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
let delete_msgs_target = context.get_delete_msgs_target().await?;
|
||||
let sync_id = match sync {
|
||||
Nosync => None,
|
||||
Sync => chat.get_sync_id(context).await?,
|
||||
@@ -617,18 +627,26 @@ impl ChatId {
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
transaction.execute(
|
||||
"UPDATE imap SET target=? WHERE rfc724_mid IN (SELECT rfc724_mid FROM msgs WHERE chat_id=?)",
|
||||
(delete_msgs_target, self,),
|
||||
"UPDATE imap SET target='' WHERE rfc724_mid IN (SELECT rfc724_mid FROM msgs WHERE chat_id=? AND rfc724_mid!='')",
|
||||
(self,),
|
||||
)?;
|
||||
transaction.execute(
|
||||
"DELETE FROM smtp WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?)",
|
||||
"UPDATE imap SET target='' WHERE rfc724_mid IN (SELECT pre_rfc724_mid FROM msgs WHERE chat_id=? AND pre_rfc724_mid!='')",
|
||||
(self,),
|
||||
)?;
|
||||
transaction.execute(
|
||||
"DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?)",
|
||||
(self,),
|
||||
)?;
|
||||
transaction.execute("DELETE FROM msgs WHERE chat_id=?", (self,))?;
|
||||
// If you change which information is preserved here, also change `MsgId::trash()`
|
||||
// and other places it references.
|
||||
transaction.execute(
|
||||
"
|
||||
INSERT OR REPLACE INTO msgs (id, rfc724_mid, pre_rfc724_mid, timestamp, chat_id, deleted)
|
||||
SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
|
||||
",
|
||||
(DC_CHAT_ID_TRASH, self),
|
||||
)?;
|
||||
transaction.execute("DELETE FROM chats_contacts WHERE chat_id=?", (self,))?;
|
||||
transaction.execute("DELETE FROM chats WHERE id=?", (self,))?;
|
||||
Ok(())
|
||||
@@ -654,7 +672,7 @@ impl ChatId {
|
||||
context
|
||||
.set_config_internal(Config::LastHousekeeping, None)
|
||||
.await?;
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1133,7 +1151,7 @@ impl ChatId {
|
||||
return Ok(stock_str::encr_none(context).await);
|
||||
}
|
||||
|
||||
let mut ret = stock_str::e2e_available(context).await + "\n";
|
||||
let mut ret = stock_str::messages_e2e_encrypted(context).await + "\n";
|
||||
|
||||
for &contact_id in get_chat_contacts(context, self)
|
||||
.await?
|
||||
@@ -1867,11 +1885,7 @@ impl Chat {
|
||||
|
||||
let (msg_text, was_truncated) = truncate_msg_text(context, msg.text.clone()).await?;
|
||||
let new_mime_headers = if msg.has_html() {
|
||||
if msg.param.exists(Param::Forwarded) {
|
||||
msg.get_id().get_html(context).await?
|
||||
} else {
|
||||
msg.param.get(Param::SendHtml).map(|s| s.to_string())
|
||||
}
|
||||
msg.param.get(Param::SendHtml).map(|s| s.to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -2107,15 +2121,16 @@ pub(crate) async fn sync(context: &Context, id: SyncId, action: SyncAction) -> R
|
||||
context
|
||||
.add_sync_item(SyncData::AlterChat { id, action })
|
||||
.await?;
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Whether the chat is pinned or archived.
|
||||
#[derive(Debug, Copy, Eq, PartialEq, Clone, Serialize, Deserialize, EnumIter)]
|
||||
#[derive(Debug, Copy, Eq, PartialEq, Clone, Serialize, Deserialize, EnumIter, Default)]
|
||||
#[repr(i8)]
|
||||
pub enum ChatVisibility {
|
||||
/// Chat is neither archived nor pinned.
|
||||
#[default]
|
||||
Normal = 0,
|
||||
|
||||
/// Chat is archived.
|
||||
@@ -2728,7 +2743,61 @@ async fn prepare_send_msg(
|
||||
Ok(row_ids)
|
||||
}
|
||||
|
||||
/// Constructs jobs for sending a message and inserts them into the appropriate table.
|
||||
/// Renders the Message or splits it into Pre- and Post-Message.
|
||||
///
|
||||
/// Pre-Message is a small message with metadata which announces a larger Post-Message.
|
||||
/// Post-Messages are not downloaded in the background.
|
||||
///
|
||||
/// If pre-message is not nessesary, this returns `None` as the 0th value.
|
||||
async fn render_mime_message_and_pre_message(
|
||||
context: &Context,
|
||||
msg: &mut Message,
|
||||
mimefactory: MimeFactory,
|
||||
) -> Result<(Option<RenderedEmail>, RenderedEmail)> {
|
||||
let needs_pre_message = msg.viewtype.has_file()
|
||||
&& mimefactory.will_be_encrypted() // unencrypted is likely email, we don't want to spam by sending multiple messages
|
||||
&& msg
|
||||
.get_filebytes(context)
|
||||
.await?
|
||||
.context("filebytes not available, even though message has attachment")?
|
||||
> PRE_MSG_ATTACHMENT_SIZE_THRESHOLD;
|
||||
|
||||
if needs_pre_message {
|
||||
info!(
|
||||
context,
|
||||
"Message {} is large and will be split into pre- and post-messages.", msg.id,
|
||||
);
|
||||
|
||||
let mut mimefactory_post_msg = mimefactory.clone();
|
||||
mimefactory_post_msg.set_as_post_message();
|
||||
let rendered_msg = mimefactory_post_msg
|
||||
.render(context)
|
||||
.await
|
||||
.context("Failed to render post-message")?;
|
||||
|
||||
let mut mimefactory_pre_msg = mimefactory;
|
||||
mimefactory_pre_msg.set_as_pre_message_for(&rendered_msg);
|
||||
let rendered_pre_msg = mimefactory_pre_msg
|
||||
.render(context)
|
||||
.await
|
||||
.context("pre-message failed to render")?;
|
||||
|
||||
if rendered_pre_msg.message.len() > PRE_MSG_SIZE_WARNING_THRESHOLD {
|
||||
warn!(
|
||||
context,
|
||||
"Pre-message for message {} is larger than expected: {}.",
|
||||
msg.id,
|
||||
rendered_pre_msg.message.len()
|
||||
);
|
||||
}
|
||||
|
||||
Ok((Some(rendered_pre_msg), rendered_msg))
|
||||
} else {
|
||||
Ok((None, mimefactory.render(context).await?))
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs jobs for sending a message and inserts them into the `smtp` table.
|
||||
///
|
||||
/// Updates the message `GuaranteeE2ee` parameter and persists it
|
||||
/// in the database depending on whether the message
|
||||
@@ -2761,24 +2830,11 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
let from = context.get_primary_self_addr().await?;
|
||||
let lowercase_from = from.to_lowercase();
|
||||
|
||||
// Send BCC to self if it is enabled.
|
||||
//
|
||||
// Previous versions of Delta Chat did not send BCC self
|
||||
// if DeleteServerAfter was set to immediately delete messages
|
||||
// from the server. This is not the case anymore
|
||||
// because BCC-self messages are also used to detect
|
||||
// that message was sent if SMTP server is slow to respond
|
||||
// and connection is frequently lost
|
||||
// before receiving status line. NB: This is not a problem for chatmail servers, so `BccSelf`
|
||||
// disabled by default is fine.
|
||||
//
|
||||
// `from` must be the last addr, see `receive_imf_inner()` why.
|
||||
recipients.retain(|x| x.to_lowercase() != lowercase_from);
|
||||
if (context.get_config_bool(Config::BccSelf).await?
|
||||
|| msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage)
|
||||
&& (context.get_config_delete_server_after().await? != Some(0) || !recipients.is_empty())
|
||||
if context.get_config_bool(Config::BccSelf).await?
|
||||
|| msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage
|
||||
{
|
||||
recipients.push(from);
|
||||
smtp::add_self_recipients(context, &mut recipients, needs_encryption).await?;
|
||||
}
|
||||
|
||||
// Default Webxdc integrations are hidden messages and must not be sent out
|
||||
@@ -2799,13 +2855,32 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let rendered_msg = match mimefactory.render(context).await {
|
||||
Ok(res) => Ok(res),
|
||||
Err(err) => {
|
||||
message::set_msg_failed(context, msg, &err.to_string()).await?;
|
||||
Err(err)
|
||||
}
|
||||
}?;
|
||||
let (rendered_pre_msg, rendered_msg) =
|
||||
match render_mime_message_and_pre_message(context, msg, mimefactory).await {
|
||||
Ok(res) => Ok(res),
|
||||
Err(err) => {
|
||||
message::set_msg_failed(context, msg, &err.to_string()).await?;
|
||||
Err(err)
|
||||
}
|
||||
}?;
|
||||
|
||||
if let (post_msg, Some(pre_msg)) = (&rendered_msg, &rendered_pre_msg) {
|
||||
info!(
|
||||
context,
|
||||
"Message {} sizes: pre-message: {}; post-message: {}.",
|
||||
msg.id,
|
||||
format_size(pre_msg.message.len(), BINARY),
|
||||
format_size(post_msg.message.len(), BINARY),
|
||||
);
|
||||
msg.pre_rfc724_mid = pre_msg.rfc724_mid.clone();
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"Message {} will be sent in one shot (no pre- and post-message). Size: {}.",
|
||||
msg.id,
|
||||
format_size(rendered_msg.message.len(), BINARY),
|
||||
);
|
||||
}
|
||||
|
||||
if needs_encryption && !rendered_msg.is_encrypted {
|
||||
/* unrecoverable */
|
||||
@@ -2844,38 +2919,48 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET subject=?, param=? WHERE id=?",
|
||||
(&msg.subject, msg.param.to_string(), msg.id),
|
||||
"UPDATE msgs SET pre_rfc724_mid=?, subject=?, param=? WHERE id=?",
|
||||
(
|
||||
&msg.pre_rfc724_mid,
|
||||
&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();
|
||||
|
||||
if let Some(sync_ids) = rendered_msg.sync_ids_to_delete {
|
||||
t.execute(
|
||||
&format!("DELETE FROM multi_device_sync WHERE id IN ({sync_ids})"),
|
||||
(),
|
||||
)?;
|
||||
t.execute(
|
||||
"INSERT INTO imap_send (mime, msg_id) VALUES (?, ?)",
|
||||
(&rendered_msg.message, msg.id),
|
||||
)?;
|
||||
} else {
|
||||
for recipients_chunk in recipients.chunks(chunk_size) {
|
||||
let recipients_chunk = recipients_chunk.join(" ");
|
||||
let row_id = t.execute(
|
||||
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id) \
|
||||
VALUES (?1, ?2, ?3, ?4)",
|
||||
(
|
||||
&rendered_msg.rfc724_mid,
|
||||
recipients_chunk,
|
||||
&rendered_msg.message,
|
||||
msg.id,
|
||||
),
|
||||
)?;
|
||||
}
|
||||
let mut stmt = t.prepare(
|
||||
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id)
|
||||
VALUES (?1, ?2, ?3, ?4)",
|
||||
)?;
|
||||
for recipients_chunk in recipients.chunks(chunk_size) {
|
||||
let recipients_chunk = recipients_chunk.join(" ");
|
||||
if let Some(pre_msg) = &rendered_pre_msg {
|
||||
let row_id = stmt.execute((
|
||||
&pre_msg.rfc724_mid,
|
||||
&recipients_chunk,
|
||||
&pre_msg.message,
|
||||
msg.id,
|
||||
))?;
|
||||
row_ids.push(row_id.try_into()?);
|
||||
}
|
||||
let row_id = stmt.execute((
|
||||
&rendered_msg.rfc724_mid,
|
||||
&recipients_chunk,
|
||||
&rendered_msg.message,
|
||||
msg.id,
|
||||
))?;
|
||||
row_ids.push(row_id.try_into()?);
|
||||
}
|
||||
Ok(row_ids)
|
||||
};
|
||||
@@ -3090,7 +3175,7 @@ pub async fn get_chat_msgs_ex(
|
||||
WHERE m.chat_id=?
|
||||
AND m.hidden=0
|
||||
AND (
|
||||
m.param GLOB \"*S=*\"
|
||||
m.param GLOB '*\nS=*' OR param GLOB 'S=*'
|
||||
OR m.from_id == ?
|
||||
OR m.to_id == ?
|
||||
);",
|
||||
@@ -3116,6 +3201,36 @@ pub async fn get_chat_msgs_ex(
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
/// Marks all unread messages in all chats as noticed.
|
||||
/// Ignores messages from blocked contacts, but does not ignore messages in muted chats.
|
||||
pub async fn marknoticed_all_chats(context: &Context) -> Result<()> {
|
||||
// The sql statement here is similar to the one in get_fresh_msgs
|
||||
let list = context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT DISTINCT(c.id)
|
||||
FROM msgs m
|
||||
INNER JOIN chats c
|
||||
ON m.chat_id=c.id
|
||||
WHERE m.state=?
|
||||
AND m.hidden=0
|
||||
AND m.chat_id>9
|
||||
AND c.blocked=0;",
|
||||
(MessageState::InFresh,),
|
||||
|row| {
|
||||
let msg_id: ChatId = row.get(0)?;
|
||||
Ok(msg_id)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
for chat_id in list {
|
||||
marknoticed_chat(context, chat_id).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Marks all messages in the chat as noticed.
|
||||
/// If the given chat-id is the archive-link, marks all messages in all archived chats as noticed.
|
||||
pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()> {
|
||||
@@ -3177,7 +3292,7 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
|
||||
let hidden_messages = context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT id, rfc724_mid FROM msgs
|
||||
"SELECT id FROM msgs
|
||||
WHERE state=?
|
||||
AND hidden=1
|
||||
AND chat_id=?
|
||||
@@ -3185,16 +3300,11 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
|
||||
(MessageState::InFresh, chat_id), // No need to check for InNoticed messages, because reactions are never InNoticed
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
let rfc724_mid: String = row.get(1)?;
|
||||
Ok((msg_id, rfc724_mid))
|
||||
Ok(msg_id)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
for (msg_id, rfc724_mid) in &hidden_messages {
|
||||
message::update_msg_state(context, *msg_id, MessageState::InSeen).await?;
|
||||
imap::markseen_on_imap_table(context, rfc724_mid).await?;
|
||||
}
|
||||
|
||||
message::markseen_msgs(context, hidden_messages).await?;
|
||||
if noticed_msgs_count == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -3216,6 +3326,10 @@ pub(crate) async fn mark_old_messages_as_noticed(
|
||||
context: &Context,
|
||||
mut msgs: Vec<ReceivedMsg>,
|
||||
) -> Result<()> {
|
||||
if context.get_config_bool(Config::TeamProfile).await? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
msgs.retain(|m| m.state.is_outgoing());
|
||||
if msgs.is_empty() {
|
||||
return Ok(());
|
||||
@@ -3837,7 +3951,7 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
.log_err(context)
|
||||
.is_ok()
|
||||
{
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
}
|
||||
}
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
@@ -3998,7 +4112,7 @@ pub async fn remove_contact_from_chat(
|
||||
} else {
|
||||
let mut sync = Nosync;
|
||||
|
||||
if chat.is_promoted() {
|
||||
if chat.is_promoted() && chat.typ != Chattype::OutBroadcast {
|
||||
remove_from_chat_contacts_table(context, chat_id, contact_id).await?;
|
||||
} else {
|
||||
remove_from_chat_contacts_table_without_trace(context, chat_id, contact_id).await?;
|
||||
@@ -4248,16 +4362,30 @@ pub async fn forward_msgs_2ctx(
|
||||
msg.param = Params::new();
|
||||
|
||||
if msg.get_viewtype() != Viewtype::Sticker {
|
||||
let forwarded_msg_id = match ctx_src.blobdir == ctx_dst.blobdir {
|
||||
true => src_msg_id,
|
||||
false => MsgId::new_unset(),
|
||||
};
|
||||
msg.param
|
||||
.set_int(Param::Forwarded, src_msg_id.to_u32() as i32);
|
||||
.set_int(Param::Forwarded, forwarded_msg_id.to_u32() as i32);
|
||||
}
|
||||
|
||||
if msg.get_viewtype() == Viewtype::Call {
|
||||
msg.viewtype = Viewtype::Text;
|
||||
}
|
||||
msg.text += &msg.additional_text;
|
||||
|
||||
let param = &mut param;
|
||||
msg.param.steal(param, Param::File);
|
||||
|
||||
// When forwarding between different accounts, blob files must be physically copied
|
||||
// because each account has its own blob directory.
|
||||
if ctx_src.blobdir == ctx_dst.blobdir {
|
||||
msg.param.steal(param, Param::File);
|
||||
} else if let Some(src_path) = param.get_file_path(ctx_src)? {
|
||||
let new_blob = BlobObject::create_and_deduplicate(ctx_dst, &src_path, &src_path)
|
||||
.context("Failed to copy blob file to destination account")?;
|
||||
msg.param.set(Param::File, new_blob.as_name());
|
||||
}
|
||||
msg.param.steal(param, Param::Filename);
|
||||
msg.param.steal(param, Param::Width);
|
||||
msg.param.steal(param, Param::Height);
|
||||
@@ -4266,6 +4394,9 @@ pub async fn forward_msgs_2ctx(
|
||||
msg.param.steal(param, Param::ProtectQuote);
|
||||
msg.param.steal(param, Param::Quote);
|
||||
msg.param.steal(param, Param::Summary1);
|
||||
if msg.has_html() {
|
||||
msg.set_html(src_msg_id.get_html(ctx_src).await?);
|
||||
}
|
||||
msg.in_reply_to = None;
|
||||
|
||||
// do not leak data as group names; a default subject is generated by mimefactory
|
||||
@@ -4311,7 +4442,7 @@ pub async fn save_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4332,12 +4463,16 @@ pub(crate) async fn save_copy_in_self_talk(
|
||||
msg.param.remove(Param::WebxdcDocumentTimestamp);
|
||||
msg.param.remove(Param::WebxdcSummary);
|
||||
msg.param.remove(Param::WebxdcSummaryTimestamp);
|
||||
msg.param.remove(Param::PostMessageFileBytes);
|
||||
msg.param.remove(Param::PostMessageViewtype);
|
||||
|
||||
msg.text += &msg.additional_text;
|
||||
|
||||
if !msg.original_msg_id.is_unset() {
|
||||
bail!("message already saved.");
|
||||
}
|
||||
|
||||
let copy_fields = "from_id, to_id, timestamp_rcvd, type, txt,
|
||||
let copy_fields = "from_id, to_id, timestamp_rcvd, type,
|
||||
mime_modified, mime_headers, mime_compressed, mime_in_reply_to, subject, msgrmsg";
|
||||
let row_id = context
|
||||
.sql
|
||||
@@ -4345,7 +4480,7 @@ pub(crate) async fn save_copy_in_self_talk(
|
||||
&format!(
|
||||
"INSERT INTO msgs ({copy_fields},
|
||||
timestamp_sent,
|
||||
chat_id, rfc724_mid, state, timestamp, param, starred)
|
||||
txt, chat_id, rfc724_mid, state, timestamp, param, starred)
|
||||
SELECT {copy_fields},
|
||||
-- Outgoing messages on originating device
|
||||
-- have timestamp_sent == 0.
|
||||
@@ -4353,10 +4488,11 @@ pub(crate) async fn save_copy_in_self_talk(
|
||||
-- so UIs display the same timestamp
|
||||
-- for saved and original message.
|
||||
IIF(timestamp_sent == 0, timestamp, timestamp_sent),
|
||||
?, ?, ?, ?, ?, ?
|
||||
?, ?, ?, ?, ?, ?, ?
|
||||
FROM msgs WHERE id=?;"
|
||||
),
|
||||
(
|
||||
msg.text,
|
||||
dest_chat_id,
|
||||
dest_rfc724_mid,
|
||||
if msg.from_id == ContactId::SELF {
|
||||
@@ -4816,12 +4952,20 @@ async fn set_contacts_by_fingerprints(
|
||||
context
|
||||
.sql
|
||||
.transaction(move |transaction| {
|
||||
transaction.execute("DELETE FROM chats_contacts WHERE chat_id=?", (id,))?;
|
||||
// For broadcast channels, we only add members,
|
||||
// because we don't use the membership consistency algorithm,
|
||||
// and are using sync messages as a basic way to ensure consistency between devices.
|
||||
// For groups, we also remove members,
|
||||
// because the sync message is used in order to sync unpromoted groups.
|
||||
if chat.typ != Chattype::OutBroadcast {
|
||||
transaction.execute("DELETE FROM chats_contacts WHERE chat_id=?", (id,))?;
|
||||
}
|
||||
|
||||
// We do not care about `add_timestamp` column
|
||||
// because timestamps are not used for broadcast channels.
|
||||
let mut statement = transaction
|
||||
.prepare("INSERT INTO chats_contacts (chat_id, contact_id) VALUES (?, ?)")?;
|
||||
let mut statement = transaction.prepare(
|
||||
"INSERT OR IGNORE INTO chats_contacts (chat_id, contact_id) VALUES (?, ?)",
|
||||
)?;
|
||||
for contact_id in &contacts {
|
||||
statement.execute((id, contact_id))?;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
use crate::Event;
|
||||
use crate::chatlist::get_archived_cnt;
|
||||
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS};
|
||||
use crate::ephemeral::Timer;
|
||||
@@ -1240,6 +1241,96 @@ async fn test_unarchive_if_muted() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_marknoticed_all_chats() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
tcm.section("alice: create chats & promote them by sending a message");
|
||||
|
||||
let alice_chat_normal = alice
|
||||
.create_group_with_members("Chat (normal)", &[alice, bob])
|
||||
.await;
|
||||
send_text_msg(alice, alice_chat_normal, "Hi".to_string()).await?;
|
||||
|
||||
let alice_chat_muted = alice
|
||||
.create_group_with_members("Chat (muted)", &[alice, bob])
|
||||
.await;
|
||||
send_text_msg(alice, alice_chat_muted, "Hi".to_string()).await?;
|
||||
set_muted(&alice.ctx, alice_chat_muted, MuteDuration::Forever).await?;
|
||||
|
||||
let alice_chat_archived_and_muted = alice
|
||||
.create_group_with_members("Chat (archived and muted)", &[alice, bob])
|
||||
.await;
|
||||
send_text_msg(alice, alice_chat_archived_and_muted, "Hi".to_string()).await?;
|
||||
set_muted(
|
||||
&alice.ctx,
|
||||
alice_chat_archived_and_muted,
|
||||
MuteDuration::Forever,
|
||||
)
|
||||
.await?;
|
||||
alice_chat_archived_and_muted
|
||||
.set_visibility(&alice.ctx, ChatVisibility::Archived)
|
||||
.await?;
|
||||
|
||||
tcm.section("bob: receive messages, accept all chats and send a reply to each messsage");
|
||||
|
||||
while let Some(sent_msg) = alice.pop_sent_msg_opt(Duration::default()).await {
|
||||
let bob_message = bob.recv_msg(&sent_msg).await;
|
||||
let bob_chat_id = bob_message.chat_id;
|
||||
bob_chat_id.accept(bob).await?;
|
||||
send_text_msg(bob, bob_chat_id, "reply".to_string()).await?;
|
||||
}
|
||||
|
||||
tcm.section("alice: receive replies from bob");
|
||||
while let Some(sent_msg) = bob.pop_sent_msg_opt(Duration::default()).await {
|
||||
alice.recv_msg(&sent_msg).await;
|
||||
}
|
||||
// ensure chats have unread messages
|
||||
assert_eq!(alice_chat_normal.get_fresh_msg_cnt(alice).await?, 1);
|
||||
assert_eq!(alice_chat_muted.get_fresh_msg_cnt(alice).await?, 1);
|
||||
assert_eq!(
|
||||
alice_chat_archived_and_muted
|
||||
.get_fresh_msg_cnt(alice)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
|
||||
tcm.section("alice: mark as read");
|
||||
alice.evtracker.clear_events();
|
||||
marknoticed_all_chats(alice).await?;
|
||||
tcm.section("alice: check that chats are no longer unread and that chatlist update events were received");
|
||||
assert_eq!(alice_chat_normal.get_fresh_msg_cnt(alice).await?, 0);
|
||||
assert_eq!(alice_chat_muted.get_fresh_msg_cnt(alice).await?, 0);
|
||||
assert_eq!(
|
||||
alice_chat_archived_and_muted
|
||||
.get_fresh_msg_cnt(alice)
|
||||
.await?,
|
||||
0
|
||||
);
|
||||
|
||||
let emitted_events = alice.evtracker.take_events();
|
||||
for event in &[
|
||||
EventType::ChatlistItemChanged {
|
||||
chat_id: Some(alice_chat_normal),
|
||||
},
|
||||
EventType::ChatlistItemChanged {
|
||||
chat_id: Some(alice_chat_muted),
|
||||
},
|
||||
EventType::ChatlistItemChanged {
|
||||
chat_id: Some(alice_chat_archived_and_muted),
|
||||
},
|
||||
EventType::ChatlistItemChanged {
|
||||
chat_id: Some(DC_CHAT_ID_ARCHIVED_LINK),
|
||||
},
|
||||
] {
|
||||
assert!(emitted_events.iter().any(|Event { typ, .. }| typ == event));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_archive_fresh_msgs() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
@@ -2862,6 +2953,123 @@ async fn test_broadcast_multidev() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that, if the broadcast channel owner has multiple devices
|
||||
/// and they have diverging views on the recipients,
|
||||
/// it is synced when sending a member-addition message.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_broadcast_recipients_sync1() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice1 = &tcm.alice().await;
|
||||
let alice2 = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
for a in &[alice1, alice2] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
|
||||
// Alice1 creates a broadcast and adds Bob, but for some reason
|
||||
// (e.g. because alice2 runs an older version of DC),
|
||||
// Alice2 doesn't get to know about it
|
||||
let a1_broadcast_id = create_broadcast(alice1, "Channel".to_string()).await?;
|
||||
alice1.send_sync_msg().await.unwrap();
|
||||
alice1.pop_sent_msg().await;
|
||||
|
||||
let qr = get_securejoin_qr(alice1, Some(a1_broadcast_id))
|
||||
.await
|
||||
.unwrap();
|
||||
tcm.exec_securejoin_qr(bob, alice1, &qr).await;
|
||||
|
||||
// The first sync message got lost, so, alice2 doesn't know about the channel now
|
||||
sync(alice1, alice2).await;
|
||||
let a2_chatlist = Chatlist::try_load(alice2, 0, Some("Channel"), None).await?;
|
||||
assert!(a2_chatlist.is_empty());
|
||||
|
||||
// Alice1 adds Charlie to the broadcast channel,
|
||||
// and now, Alice2 receives the messages
|
||||
join_securejoin(charlie, &qr).await.unwrap();
|
||||
|
||||
let request = charlie.pop_sent_msg().await;
|
||||
alice1.recv_msg_trash(&request).await;
|
||||
alice2.recv_msg_trash(&request).await;
|
||||
|
||||
let auth_required = alice1.pop_sent_msg().await;
|
||||
charlie.recv_msg_trash(&auth_required).await;
|
||||
alice2.recv_msg_trash(&auth_required).await;
|
||||
|
||||
let request_with_auth = charlie.pop_sent_msg().await;
|
||||
alice1.recv_msg_trash(&request_with_auth).await;
|
||||
alice2.recv_msg_trash(&request_with_auth).await;
|
||||
|
||||
let member_added = alice1.pop_sent_msg().await;
|
||||
let a2_member_added = alice2.recv_msg(&member_added).await;
|
||||
let _c_member_added = charlie.recv_msg(&member_added).await;
|
||||
|
||||
// Alice1 will now sync the full member list to Alice2:
|
||||
sync(alice1, alice2).await;
|
||||
let a2_chatlist = Chatlist::try_load(alice2, 0, Some("Channel"), None).await?;
|
||||
assert_eq!(a2_chatlist.get_msg_id(0)?.unwrap(), a2_member_added.id);
|
||||
|
||||
let a2_bob_contact = alice2.add_or_lookup_contact_id(bob).await;
|
||||
let a2_charlie_contact = alice2.add_or_lookup_contact_id(charlie).await;
|
||||
|
||||
let a2_chat_members = get_chat_contacts(alice2, a2_member_added.chat_id).await?;
|
||||
assert!(a2_chat_members.contains(&a2_bob_contact));
|
||||
assert!(a2_chat_members.contains(&a2_charlie_contact));
|
||||
assert_eq!(a2_chat_members.len(), 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that, if the broadcast channel owner has multiple devices
|
||||
/// and they have diverging views on the recipients,
|
||||
/// sync messages only add members but don't remove them.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_broadcast_recipients_sync2() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice1 = &tcm.alice().await;
|
||||
let alice2 = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
for a in &[alice1, alice2] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
|
||||
let a1_broadcast_id = create_broadcast(alice1, "Channel".to_string()).await?;
|
||||
sync(alice1, alice2).await;
|
||||
|
||||
tcm.section("Alice1 adds Bob, but Alice2 misses it for some reason");
|
||||
let qr = get_securejoin_qr(alice1, Some(a1_broadcast_id))
|
||||
.await
|
||||
.unwrap();
|
||||
tcm.exec_securejoin_qr(bob, alice1, &qr).await;
|
||||
|
||||
tcm.section("Alice2 adds Charlie, but Alice1 misses it for some reason");
|
||||
let a2_broadcast_id = Chatlist::try_load(alice2, 0, Some("Channel"), None)
|
||||
.await?
|
||||
.get_chat_id(0)
|
||||
.unwrap();
|
||||
let qr = get_securejoin_qr(alice2, Some(a2_broadcast_id))
|
||||
.await
|
||||
.unwrap();
|
||||
tcm.exec_securejoin_qr(charlie, alice2, &qr).await;
|
||||
|
||||
tcm.section("The sync messages should correct the problem");
|
||||
sync(alice1, alice2).await;
|
||||
sync(alice2, alice1).await;
|
||||
|
||||
for (alice, broadcast_id) in [(alice1, a1_broadcast_id), (alice2, a2_broadcast_id)] {
|
||||
let bob_contact = alice.add_or_lookup_contact_id(bob).await;
|
||||
let charlie_contact = alice.add_or_lookup_contact_id(charlie).await;
|
||||
|
||||
let chat_members = get_chat_contacts(alice, broadcast_id).await?;
|
||||
assert!(chat_members.contains(&bob_contact));
|
||||
assert!(chat_members.contains(&charlie_contact));
|
||||
assert_eq!(chat_members.len(), 2);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// - Create a broadcast channel
|
||||
/// - Send a message into it in order to promote it
|
||||
/// - Add a contact
|
||||
@@ -3116,7 +3324,7 @@ async fn test_broadcast_channel_protected_listid() -> Result<()> {
|
||||
.await?
|
||||
.grpid;
|
||||
|
||||
let parsed = mimeparser::MimeMessage::from_bytes(bob, sent.payload.as_bytes(), None).await?;
|
||||
let parsed = mimeparser::MimeMessage::from_bytes(bob, sent.payload.as_bytes()).await?;
|
||||
assert_eq!(
|
||||
parsed.get_mailinglist_header().unwrap(),
|
||||
format!("My Channel <{}>", alice_list_id)
|
||||
@@ -3227,6 +3435,11 @@ async fn test_remove_member_from_broadcast() -> Result<()> {
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
remove_contact_from_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
|
||||
// Alice must not remember old members,
|
||||
// because we would like to remember the minimum information possible
|
||||
let past_contacts = get_past_chat_contacts(alice, alice_chat_id).await?;
|
||||
assert_eq!(past_contacts.len(), 0);
|
||||
|
||||
let remove_msg = alice.pop_sent_msg().await;
|
||||
let rcvd = bob.recv_msg(&remove_msg).await;
|
||||
assert_eq!(rcvd.text, "Member Me removed by alice@example.org.");
|
||||
@@ -3311,7 +3524,7 @@ async fn test_leave_broadcast_multidevice() -> Result<()> {
|
||||
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?;
|
||||
let parsed = MimeMessage::from_bytes(bob1, leave_msg.payload().as_bytes()).await?;
|
||||
assert_eq!(parsed.parts[0].msg, "I left the group.");
|
||||
|
||||
let rcvd = bob1.recv_msg(&leave_msg).await;
|
||||
@@ -3603,13 +3816,13 @@ async fn test_chat_get_encryption_info() -> Result<()> {
|
||||
let chat_id = create_group(alice, "Group").await?;
|
||||
assert_eq!(
|
||||
chat_id.get_encryption_info(alice).await?,
|
||||
"End-to-end encryption available"
|
||||
"Messages are end-to-end encrypted."
|
||||
);
|
||||
|
||||
add_contact_to_chat(alice, chat_id, contact_bob).await?;
|
||||
assert_eq!(
|
||||
chat_id.get_encryption_info(alice).await?,
|
||||
"End-to-end encryption available\n\
|
||||
"Messages are end-to-end encrypted.\n\
|
||||
\n\
|
||||
bob@example.net\n\
|
||||
CCCB 5AA9 F6E1 141C 9431\n\
|
||||
@@ -3619,7 +3832,7 @@ async fn test_chat_get_encryption_info() -> Result<()> {
|
||||
add_contact_to_chat(alice, chat_id, contact_fiona).await?;
|
||||
assert_eq!(
|
||||
chat_id.get_encryption_info(alice).await?,
|
||||
"End-to-end encryption available\n\
|
||||
"Messages are end-to-end encrypted.\n\
|
||||
\n\
|
||||
fiona@example.net\n\
|
||||
C8BA 50BF 4AC1 2FAF 38D7\n\
|
||||
@@ -3633,13 +3846,13 @@ async fn test_chat_get_encryption_info() -> Result<()> {
|
||||
let email_chat = alice.create_email_chat(bob).await;
|
||||
assert_eq!(
|
||||
email_chat.id.get_encryption_info(alice).await?,
|
||||
"No encryption"
|
||||
"No encryption."
|
||||
);
|
||||
|
||||
alice.sql.execute("DELETE FROM public_keys", ()).await?;
|
||||
assert_eq!(
|
||||
chat_id.get_encryption_info(alice).await?,
|
||||
"End-to-end encryption available\n\
|
||||
"Messages are end-to-end encrypted.\n\
|
||||
\n\
|
||||
fiona@example.net\n\
|
||||
(key missing)\n\
|
||||
@@ -5278,6 +5491,97 @@ async fn test_forward_msgs_2ctx() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_forward_msgs_2ctx_with_file() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
// First, establish a chat between Alice and Bob to have the chat IDs
|
||||
let alice_chat = alice.create_chat(bob).await;
|
||||
let alice_initial = alice.send_text(alice_chat.id, "hi").await;
|
||||
let bob_alice_msg = bob.recv_msg(&alice_initial).await;
|
||||
let bob_chat_id = bob_alice_msg.chat_id;
|
||||
bob_chat_id.accept(bob).await?;
|
||||
|
||||
// Alice sends a message with an attached file to her self-chat
|
||||
let alice_self_chat = alice.get_self_chat().await;
|
||||
let file_bytes = b"test file content";
|
||||
let file = alice.get_blobdir().join("test.txt");
|
||||
tokio::fs::write(&file, file_bytes).await?;
|
||||
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file_and_deduplicate(alice, &file, Some("test.txt"), Some("text/plain"))?;
|
||||
msg.set_text("Here's a file".to_string());
|
||||
|
||||
alice.send_msg(alice_self_chat.id, &mut msg).await;
|
||||
let alice_self_msg = alice.get_last_msg().await;
|
||||
|
||||
// Verify the file exists in Alice's blobdir
|
||||
assert_eq!(alice_self_msg.viewtype, Viewtype::File);
|
||||
let alice_original_file_path = alice_self_msg.get_file(alice).unwrap();
|
||||
let alice_original_content = tokio::fs::read(&alice_original_file_path).await?;
|
||||
assert_eq!(alice_original_content, file_bytes);
|
||||
|
||||
// Alice forwards the message to Bob using forward_msgs_2ctx
|
||||
forward_msgs_2ctx(alice, &[alice_self_msg.id], bob, bob_chat_id).await?;
|
||||
|
||||
// Bob should have the forwarded message with the file in his database
|
||||
let bob_msg = bob.get_last_msg().await;
|
||||
assert_eq!(bob_msg.viewtype, Viewtype::File);
|
||||
assert!(bob_msg.is_forwarded());
|
||||
assert_eq!(bob_msg.text, "Here's a file");
|
||||
assert_eq!(bob_msg.from_id, ContactId::SELF);
|
||||
|
||||
// Verify Bob has the file in his blobdir with correct content
|
||||
let bob_file_path = bob_msg.get_file(bob).unwrap();
|
||||
let bob_file_content = tokio::fs::read(&bob_file_path).await?;
|
||||
assert_eq!(bob_file_content, file_bytes);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_forward_msgs_2ctx_missing_blob() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_chat = alice.create_chat(bob).await;
|
||||
let alice_initial = alice.send_text(alice_chat.id, "hi").await;
|
||||
let bob_alice_msg = bob.recv_msg(&alice_initial).await;
|
||||
let bob_chat_id = bob_alice_msg.chat_id;
|
||||
bob_chat_id.accept(bob).await?;
|
||||
// Alice sends a file to her self-chat
|
||||
let alice_self_chat = alice.get_self_chat().await;
|
||||
let file_bytes = b"test content";
|
||||
let file = alice.get_blobdir().join("test.txt");
|
||||
tokio::fs::write(&file, file_bytes).await?;
|
||||
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file_and_deduplicate(alice, &file, Some("test.txt"), Some("text/plain"))?;
|
||||
msg.set_text("File message".to_string());
|
||||
|
||||
alice.send_msg(alice_self_chat.id, &mut msg).await;
|
||||
let alice_self_msg = alice.get_last_msg().await;
|
||||
|
||||
// Delete the blob file from Alice's blobdir to simulate a missing file
|
||||
let alice_file_path = alice_self_msg.get_file(alice).unwrap();
|
||||
tokio::fs::remove_file(&alice_file_path).await?;
|
||||
|
||||
// Alice tries to forward the message - this should fail with an error
|
||||
let result = forward_msgs_2ctx(alice, &[alice_self_msg.id], bob, bob_chat_id).await;
|
||||
assert!(result.is_err());
|
||||
assert!(
|
||||
result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("Failed to copy blob file")
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that in multi-device setup
|
||||
/// second device learns the key of a contact
|
||||
/// via Autocrypt-Gossip in 1:1 chats.
|
||||
|
||||
@@ -76,7 +76,7 @@ impl Chatlist {
|
||||
/// the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are *any* archived
|
||||
/// chats
|
||||
/// - the flag DC_GCL_FOR_FORWARDING sorts "Saved messages" to the top of the chatlist
|
||||
/// and hides the device-chat and contact requests
|
||||
/// and hides the device-chat, contact requests and incoming broadcasts.
|
||||
/// typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
|
||||
/// - if the flag DC_GCL_NO_SPECIALS is set, archive link is not added
|
||||
/// to the list (may be used eg. for selecting chats on forwarding, the flag is
|
||||
@@ -224,8 +224,9 @@ impl Chatlist {
|
||||
let process_rows = |rows: rusqlite::AndThenRows<_>| {
|
||||
rows.filter_map(|row: std::result::Result<(_, _, Params, _), _>| match row {
|
||||
Ok((chat_id, typ, param, msg_id)) => {
|
||||
if typ == Chattype::Mailinglist
|
||||
&& param.get(Param::ListPost).is_none_or_empty()
|
||||
if typ == Chattype::InBroadcast
|
||||
|| (typ == Chattype::Mailinglist
|
||||
&& param.get(Param::ListPost).is_none_or_empty())
|
||||
{
|
||||
None
|
||||
} else {
|
||||
@@ -396,8 +397,6 @@ impl Chatlist {
|
||||
if lastmsg.from_id == ContactId::SELF {
|
||||
None
|
||||
} else if chat.typ == Chattype::Group
|
||||
|| chat.typ == Chattype::OutBroadcast
|
||||
|| chat.typ == Chattype::InBroadcast
|
||||
|| chat.typ == Chattype::Mailinglist
|
||||
|| chat.is_self_talk()
|
||||
{
|
||||
@@ -471,10 +470,11 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::chat::save_msgs;
|
||||
use crate::chat::{
|
||||
add_contact_to_chat, create_group, get_chat_contacts, remove_contact_from_chat,
|
||||
send_text_msg, set_chat_name,
|
||||
add_contact_to_chat, create_broadcast, create_group, get_chat_contacts,
|
||||
remove_contact_from_chat, send_text_msg, set_chat_name,
|
||||
};
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::stock_str::StockMessage;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::TestContextManager;
|
||||
@@ -598,6 +598,41 @@ mod tests {
|
||||
assert_eq!(chats.len(), 1);
|
||||
}
|
||||
|
||||
/// Test that DC_CHAT_TYPE_IN_BROADCAST are hidden
|
||||
/// and DC_CHAT_TYPE_OUT_BROADCAST are shown in chatlist for forwarding.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_broadcast_visiblity_on_forward() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_broadcast_a_id = create_broadcast(alice, "Channel Alice".to_string()).await?;
|
||||
let qr = get_securejoin_qr(alice, Some(alice_broadcast_a_id))
|
||||
.await
|
||||
.unwrap();
|
||||
let bob_broadcast_a_id = tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
||||
let bob_broadcast_b_id = create_broadcast(bob, "Channel Bob".to_string()).await?;
|
||||
|
||||
let chats = Chatlist::try_load(bob, DC_GCL_FOR_FORWARDING, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
!chats
|
||||
.iter()
|
||||
.any(|(chat_id, _)| chat_id == &bob_broadcast_a_id),
|
||||
"alice broadcast is not shown in bobs forwarding chatlist"
|
||||
);
|
||||
assert!(
|
||||
chats
|
||||
.iter()
|
||||
.any(|(chat_id, _)| chat_id == &bob_broadcast_b_id),
|
||||
"bobs own broadcast is shown in his forwarding chatlist"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_search_special_chat_names() {
|
||||
let t = TestContext::new_alice().await;
|
||||
@@ -806,6 +841,32 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_summary_prefix_for_channel() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
let alice_chat_id = create_broadcast(&alice, "alice's channel".to_string()).await?;
|
||||
let qr = get_securejoin_qr(&alice, Some(alice_chat_id)).await?;
|
||||
tcm.exec_securejoin_qr(&bob, &alice, &qr).await;
|
||||
|
||||
send_text_msg(&alice, alice_chat_id, "hi".into()).await?;
|
||||
let sent1 = alice.pop_sent_msg().await;
|
||||
let chatlist = Chatlist::try_load(&alice, 0, None, None).await?;
|
||||
let summary = chatlist.get_summary(&alice, 0, None).await?;
|
||||
assert!(summary.prefix.is_none());
|
||||
assert_eq!(summary.text, "hi");
|
||||
|
||||
bob.recv_msg(&sent1).await;
|
||||
let chatlist = Chatlist::try_load(&bob, 0, None, None).await?;
|
||||
let summary = chatlist.get_summary(&bob, 0, None).await?;
|
||||
assert!(summary.prefix.is_none());
|
||||
assert_eq!(summary.text, "hi");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_load_broken() {
|
||||
let t = TestContext::new_bob().await;
|
||||
|
||||
@@ -175,11 +175,6 @@ pub enum Config {
|
||||
#[strum(props(default = "0"))] // also change MediaQuality.default() on changes
|
||||
MediaQuality,
|
||||
|
||||
/// If set to "1", then existing messages are considered to be already fetched.
|
||||
/// This flag is reset after successful configuration.
|
||||
#[strum(props(default = "1"))]
|
||||
FetchedExistingMsgs,
|
||||
|
||||
/// Timer in seconds after which the message is deleted from the
|
||||
/// server.
|
||||
///
|
||||
@@ -199,10 +194,6 @@ pub enum Config {
|
||||
#[strum(props(default = "0"))]
|
||||
DeleteDeviceAfter,
|
||||
|
||||
/// Move messages to the Trash folder instead of marking them "\Deleted". Overrides
|
||||
/// `ProviderOptions::delete_to_trash`.
|
||||
DeleteToTrash,
|
||||
|
||||
/// The primary email address.
|
||||
ConfiguredAddr,
|
||||
|
||||
@@ -280,9 +271,6 @@ pub enum Config {
|
||||
/// Configured folder for chat messages.
|
||||
ConfiguredMvboxFolder,
|
||||
|
||||
/// Configured "Trash" folder.
|
||||
ConfiguredTrashFolder,
|
||||
|
||||
/// Unix timestamp of the last successful configuration.
|
||||
ConfiguredTimestamp,
|
||||
|
||||
@@ -354,7 +342,17 @@ pub enum Config {
|
||||
DonationRequestNextCheck,
|
||||
|
||||
/// Defines the max. size (in bytes) of messages downloaded automatically.
|
||||
///
|
||||
/// For messages with large attachments, two messages are sent:
|
||||
/// a Pre-Message containing metadata and text and a Post-Message additionally
|
||||
/// containing the attachment. NB: Some "extra" metadata like avatars and gossiped
|
||||
/// encryption keys is stripped from post-messages to save traffic.
|
||||
/// Pre-Messages are shown as placeholder messages. They can be downloaded fully using
|
||||
/// `MsgId::download_full()` later. Post-Messages are automatically downloaded if they are
|
||||
/// smaller than the download_limit. Other messages are always auto-downloaded.
|
||||
///
|
||||
/// 0 = no limit.
|
||||
/// Changes only affect future messages.
|
||||
#[strum(props(default = "0"))]
|
||||
DownloadLimit,
|
||||
|
||||
@@ -438,14 +436,24 @@ pub enum Config {
|
||||
/// using this still run unmodified code.
|
||||
TestHooks,
|
||||
|
||||
/// Return an error from `receive_imf_inner()` for a fully downloaded message. For tests.
|
||||
FailOnReceivingFullMsg,
|
||||
/// Return an error from `receive_imf_inner()`. For tests.
|
||||
SimulateReceiveImfError,
|
||||
|
||||
/// Enable composing emails with Header Protection as defined in
|
||||
/// <https://www.rfc-editor.org/rfc/rfc9788.html> "Header Protection for Cryptographically
|
||||
/// Protected Email".
|
||||
#[strum(props(default = "1"))]
|
||||
StdHeaderProtectionComposing,
|
||||
|
||||
/// Who can call me.
|
||||
///
|
||||
/// The options are from the `WhoCanCallMe` enum.
|
||||
#[strum(props(default = "1"))]
|
||||
WhoCanCallMe,
|
||||
|
||||
/// Experimental option denoting that the current profile is shared between multiple team members.
|
||||
/// For now, the only effect of this option is that seen flags are not synchronized.
|
||||
TeamProfile,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -504,7 +512,7 @@ impl Context {
|
||||
.into_owned()
|
||||
})
|
||||
}
|
||||
Config::SysVersion => Some((*constants::DC_VERSION_STR).clone()),
|
||||
Config::SysVersion => Some(constants::DC_VERSION_STR.to_string()),
|
||||
Config::SysMsgsizeMaxRecommended => Some(format!("{RECOMMENDED_FILE_SIZE}")),
|
||||
Config::SysConfigKeys => Some(get_config_keys_string()),
|
||||
_ => self.sql.get_raw_config(key.as_ref()).await?,
|
||||
@@ -605,12 +613,6 @@ impl Context {
|
||||
&& !self.get_config_bool(Config::Bot).await?)
|
||||
}
|
||||
|
||||
/// Returns whether sync messages should be uploaded to the mvbox.
|
||||
pub(crate) async fn should_move_sync_msgs(&self) -> Result<bool> {
|
||||
Ok(self.get_config_bool(Config::MvboxMove).await?
|
||||
|| !self.get_config_bool(Config::IsChatmail).await?)
|
||||
}
|
||||
|
||||
/// Returns whether MDNs should be requested.
|
||||
pub(crate) async fn should_request_mdns(&self) -> Result<bool> {
|
||||
match self.get_config_bool_opt(Config::MdnsEnabled).await? {
|
||||
@@ -687,7 +689,6 @@ impl Context {
|
||||
| Config::MdnsEnabled
|
||||
| Config::MvboxMove
|
||||
| Config::OnlyFetchMvbox
|
||||
| Config::DeleteToTrash
|
||||
| Config::Configured
|
||||
| Config::Bot
|
||||
| Config::NotifyAboutWrongPw
|
||||
@@ -711,12 +712,7 @@ impl Context {
|
||||
Self::check_config(key, value)?;
|
||||
|
||||
let n_transports = self.count_transports().await?;
|
||||
if n_transports > 1
|
||||
&& matches!(
|
||||
key,
|
||||
Config::MvboxMove | Config::OnlyFetchMvbox | Config::ShowEmails
|
||||
)
|
||||
{
|
||||
if n_transports > 1 && matches!(key, Config::MvboxMove | Config::OnlyFetchMvbox) {
|
||||
bail!("Cannot reconfigure {key} when multiple transports are configured");
|
||||
}
|
||||
|
||||
@@ -880,7 +876,7 @@ impl Context {
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
self.scheduler.interrupt_inbox().await;
|
||||
self.scheduler.interrupt_smtp().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -944,7 +940,7 @@ impl Context {
|
||||
/// This should only be used by test code and during configure.
|
||||
#[cfg(test)] // AEAP is disabled, but there are still tests for it
|
||||
pub(crate) async fn set_primary_self_addr(&self, primary_new: &str) -> Result<()> {
|
||||
self.quota.write().await.take();
|
||||
self.quota.write().await.clear();
|
||||
|
||||
self.sql
|
||||
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(primary_new))
|
||||
@@ -953,7 +949,7 @@ impl Context {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns all primary and secondary self addresses.
|
||||
/// Returns the primary self address followed by all secondary ones.
|
||||
pub(crate) async fn get_all_self_addrs(&self) -> Result<Vec<String>> {
|
||||
let primary_addrs = self.get_config(Config::ConfiguredAddr).await?.into_iter();
|
||||
let secondary_addrs = self.get_secondary_self_addrs().await?.into_iter();
|
||||
@@ -989,5 +985,18 @@ fn get_config_keys_string() -> String {
|
||||
format!(" {keys} ")
|
||||
}
|
||||
|
||||
/// Returns all `ui.*` config keys that were set by the UI.
|
||||
pub async fn get_all_ui_config_keys(context: &Context) -> Result<Vec<String>> {
|
||||
let ui_keys = context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT keyname FROM config WHERE keyname GLOB 'ui.*' ORDER BY config.id",
|
||||
(),
|
||||
|row| Ok(row.get::<_, String>(0)?),
|
||||
)
|
||||
.await?;
|
||||
Ok(ui_keys)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod config_tests;
|
||||
|
||||
@@ -81,6 +81,37 @@ async fn test_ui_config() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_all_ui_config_keys() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
t.set_ui_config("ui.android.screen_security", Some("safe"))
|
||||
.await?;
|
||||
t.set_ui_config("ui.lastchatid", Some("231")).await?;
|
||||
t.set_ui_config(
|
||||
"ui.desktop.webxdcBounds.528490",
|
||||
Some(r#"{"x":954,"y":356,"width":378,"height":671}"#),
|
||||
)
|
||||
.await?;
|
||||
t.set_ui_config(
|
||||
"ui.desktop.webxdcBounds.556543",
|
||||
Some(r#"{"x":954,"y":356,"width":378,"height":671}"#),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
get_all_ui_config_keys(&t).await?,
|
||||
vec![
|
||||
"ui.android.screen_security",
|
||||
"ui.lastchatid",
|
||||
"ui.desktop.webxdcBounds.528490",
|
||||
"ui.desktop.webxdcBounds.556543"
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Regression test for https://github.com/deltachat/deltachat-core-rust/issues/3012
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_config_bool() -> Result<()> {
|
||||
@@ -237,7 +268,7 @@ async fn test_no_sync_on_self_sent_msg() -> Result<()> {
|
||||
let status = "Sent via usual message";
|
||||
alice0.set_config(Config::Selfstatus, Some(status)).await?;
|
||||
alice0.send_sync_msg().await?;
|
||||
alice0.pop_sent_sync_msg().await;
|
||||
alice0.pop_sent_msg().await;
|
||||
let status1 = "Synced via sync message";
|
||||
alice1.set_config(Config::Selfstatus, Some(status1)).await?;
|
||||
tcm.send_recv(alice0, alice1, "hi Alice!").await;
|
||||
@@ -261,7 +292,7 @@ async fn test_no_sync_on_self_sent_msg() -> Result<()> {
|
||||
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
|
||||
.await?;
|
||||
alice0.send_sync_msg().await?;
|
||||
alice0.pop_sent_sync_msg().await;
|
||||
alice0.pop_sent_msg().await;
|
||||
let file = alice1.dir.path().join("avatar.jpg");
|
||||
let bytes = include_bytes!("../../test-data/image/avatar1000x1000.jpg");
|
||||
tokio::fs::write(&file, bytes).await?;
|
||||
|
||||
@@ -23,7 +23,7 @@ use percent_encoding::utf8_percent_encode;
|
||||
use server_params::{ServerParams, expand_param_vector};
|
||||
use tokio::task;
|
||||
|
||||
use crate::config::{self, Config};
|
||||
use crate::config::Config;
|
||||
use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT;
|
||||
use crate::context::Context;
|
||||
use crate::imap::Imap;
|
||||
@@ -45,6 +45,10 @@ use crate::transport::{
|
||||
use crate::{EventType, stock_str};
|
||||
use crate::{chat, provider};
|
||||
|
||||
/// Maximum number of relays
|
||||
/// see <https://github.com/chatmail/core/issues/7608>
|
||||
pub(crate) const MAX_TRANSPORT_RELAYS: usize = 5;
|
||||
|
||||
macro_rules! progress {
|
||||
($context:tt, $progress:expr, $comment:expr) => {
|
||||
assert!(
|
||||
@@ -206,7 +210,8 @@ impl Context {
|
||||
/// (i.e. [EnteredLoginParam::addr]).
|
||||
pub async fn delete_transport(&self, addr: &str) -> Result<()> {
|
||||
let now = time();
|
||||
self.sql
|
||||
let removed_transport_id = self
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let primary_addr = transaction.query_row(
|
||||
"SELECT value FROM config WHERE keyname='configured_addr'",
|
||||
@@ -247,10 +252,11 @@ impl Context {
|
||||
(addr, remove_timestamp),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
Ok(transport_id)
|
||||
})
|
||||
.await?;
|
||||
send_sync_transports(self).await?;
|
||||
self.quota.write().await.remove(&removed_transport_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -268,18 +274,48 @@ impl Context {
|
||||
)
|
||||
.await?
|
||||
{
|
||||
if self.get_config(Config::MvboxMove).await?.as_deref() != Some("0") {
|
||||
bail!("Cannot use multi-transport with mvbox_move enabled.");
|
||||
}
|
||||
// Should be checked before `MvboxMove` because the latter makes no sense in presense of
|
||||
// `OnlyFetchMvbox` and even grayed out in the UIs in this case.
|
||||
if self.get_config(Config::OnlyFetchMvbox).await?.as_deref() != Some("0") {
|
||||
bail!("Cannot use multi-transport with only_fetch_mvbox enabled.");
|
||||
bail!(
|
||||
"To use additional relays, disable the legacy option \"Settings / Advanced / Only Fetch from DeltaChat Folder\"."
|
||||
);
|
||||
}
|
||||
if self.get_config(Config::ShowEmails).await?.as_deref() != Some("2") {
|
||||
bail!("Cannot use multi-transport with disabled fetching of classic emails.");
|
||||
if self.get_config(Config::MvboxMove).await?.as_deref() != Some("0") {
|
||||
bail!(
|
||||
"To use additional relays, disable the legacy option \"Settings / Advanced / Move automatically to DeltaChat Folder\"."
|
||||
);
|
||||
}
|
||||
|
||||
if self
|
||||
.sql
|
||||
.count("SELECT COUNT(*) FROM transports", ())
|
||||
.await?
|
||||
>= MAX_TRANSPORT_RELAYS
|
||||
{
|
||||
bail!(
|
||||
"You have reached the maximum number of relays ({}).",
|
||||
MAX_TRANSPORT_RELAYS
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let provider = configure(self, param).await?;
|
||||
let provider = match configure(self, param).await {
|
||||
Err(error) => {
|
||||
// Log entered and actual params
|
||||
let configured_param = get_configured_param(self, param).await;
|
||||
warn!(
|
||||
self,
|
||||
"configure failed: Entered params: {}. Used params: {}. Error: {error}.",
|
||||
param.to_string(),
|
||||
configured_param
|
||||
.map(|param| param.to_string())
|
||||
.unwrap_or("error".to_owned())
|
||||
);
|
||||
return Err(error);
|
||||
}
|
||||
Ok(provider) => provider,
|
||||
};
|
||||
self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
|
||||
.await?;
|
||||
on_configure_completed(self, provider).await?;
|
||||
@@ -590,8 +626,6 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
|
||||
|
||||
progress!(ctx, 920);
|
||||
|
||||
ctx.set_config_internal(Config::FetchedExistingMsgs, config::from_bool(false))
|
||||
.await?;
|
||||
ctx.scheduler.interrupt_inbox().await;
|
||||
|
||||
progress!(ctx, 940);
|
||||
|
||||
@@ -2,16 +2,13 @@
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use percent_encoding::{AsciiSet, NON_ALPHANUMERIC};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::chat::ChatId;
|
||||
|
||||
pub static DC_VERSION_STR: LazyLock<String> =
|
||||
LazyLock::new(|| env!("CARGO_PKG_VERSION").to_string());
|
||||
pub static DC_VERSION_STR: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
/// Set of characters to percent-encode in email addresses and names.
|
||||
pub(crate) const NON_ALPHANUMERIC_WITHOUT_DOT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'.');
|
||||
|
||||
@@ -1133,7 +1133,8 @@ VALUES (?, ?, ?, ?, ?, ?)
|
||||
Origin::IncomingReplyTo
|
||||
};
|
||||
if query.is_some() {
|
||||
let s3str_like_cmd = format!("%{}%", query.unwrap_or("").to_lowercase());
|
||||
let query_lowercased = query.unwrap_or("").to_lowercase();
|
||||
let s3str_like_cmd = format!("%{}%", query_lowercased);
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
@@ -1144,14 +1145,15 @@ WHERE c.id>?
|
||||
AND c.origin>=?
|
||||
AND c.blocked=0
|
||||
AND (IFNULL(c.name_normalized,IIF(c.name='',c.authname,c.name)) LIKE ? OR c.addr LIKE ?)
|
||||
ORDER BY c.last_seen DESC, c.id DESC
|
||||
ORDER BY c.origin>=? DESC, c.last_seen DESC, c.id DESC
|
||||
",
|
||||
(
|
||||
ContactId::LAST_SPECIAL,
|
||||
flag_address,
|
||||
minimal_origin,
|
||||
&s3str_like_cmd,
|
||||
&s3str_like_cmd,
|
||||
&query_lowercased,
|
||||
Origin::CreateChat,
|
||||
),
|
||||
|row| {
|
||||
let id: ContactId = row.get(0)?;
|
||||
@@ -1201,8 +1203,13 @@ ORDER BY c.last_seen DESC, c.id DESC
|
||||
AND (fingerprint='')=?
|
||||
AND origin>=?
|
||||
AND blocked=0
|
||||
ORDER BY last_seen DESC, id DESC;",
|
||||
(ContactId::LAST_SPECIAL, flag_address, minimal_origin),
|
||||
ORDER BY origin>=? DESC, last_seen DESC, id DESC",
|
||||
(
|
||||
ContactId::LAST_SPECIAL,
|
||||
flag_address,
|
||||
minimal_origin,
|
||||
Origin::CreateChat,
|
||||
),
|
||||
|row| {
|
||||
let id: ContactId = row.get(0)?;
|
||||
let addr: String = row.get(1)?;
|
||||
@@ -1292,18 +1299,6 @@ WHERE addr=?
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns number of blocked contacts.
|
||||
pub async fn get_blocked_cnt(context: &Context) -> Result<usize> {
|
||||
let count = context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM contacts WHERE id>? AND blocked!=0",
|
||||
(ContactId::LAST_SPECIAL,),
|
||||
)
|
||||
.await?;
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// Get blocked contacts.
|
||||
pub async fn get_all_blocked(context: &Context) -> Result<Vec<ContactId>> {
|
||||
Contact::update_blocked_mailinglist_contacts(context)
|
||||
@@ -1347,13 +1342,13 @@ WHERE addr=?
|
||||
let fingerprint_other = fingerprint_other.to_string();
|
||||
|
||||
let stock_message = if contact.public_key(context).await?.is_some() {
|
||||
stock_str::e2e_available(context).await
|
||||
stock_str::messages_e2e_encrypted(context).await
|
||||
} else {
|
||||
stock_str::encr_none(context).await
|
||||
};
|
||||
|
||||
let finger_prints = stock_str::finger_prints(context).await;
|
||||
let mut ret = format!("{stock_message}.\n{finger_prints}:");
|
||||
let mut ret = format!("{stock_message}\n{finger_prints}:");
|
||||
|
||||
let fingerprint_self = load_self_public_key(context)
|
||||
.await?
|
||||
@@ -1459,7 +1454,7 @@ WHERE addr=?
|
||||
/// Returns true if the contact is a key-contact.
|
||||
/// Otherwise it is an addresss-contact.
|
||||
pub fn is_key_contact(&self) -> bool {
|
||||
self.fingerprint.is_some()
|
||||
self.fingerprint.is_some() || self.id == ContactId::SELF
|
||||
}
|
||||
|
||||
/// Returns OpenPGP fingerprint of a contact.
|
||||
@@ -1652,8 +1647,7 @@ WHERE addr=?
|
||||
///
|
||||
/// If this returns Some(_),
|
||||
/// display green checkmark in the profile and "Introduced by ..." line
|
||||
/// with the name and address of the contact
|
||||
/// formatted by [Self::get_name_n_addr].
|
||||
/// with the name of the contact.
|
||||
///
|
||||
/// If this returns `Some(None)`, then the contact is verified,
|
||||
/// but it's unclear by whom.
|
||||
|
||||
@@ -85,10 +85,15 @@ async fn test_get_contacts() -> Result<()> {
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(contacts.first(), Some(&id));
|
||||
|
||||
// Search by address.
|
||||
// Search by address is case-insensitive, but only returns direct matches.
|
||||
let contacts = Contact::get_all(&context, 0, Some("alice@example.org")).await?;
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(contacts.first(), Some(&id));
|
||||
let contacts = Contact::get_all(&context, 0, Some("Alice@example.org")).await?;
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(contacts.first(), Some(&id));
|
||||
let contacts = Contact::get_all(&context, 0, Some("alice@")).await?;
|
||||
assert_eq!(contacts.len(), 0);
|
||||
|
||||
let contacts = Contact::get_all(&context, 0, Some("Foobar")).await?;
|
||||
assert_eq!(contacts.len(), 0);
|
||||
@@ -818,7 +823,7 @@ async fn test_contact_get_encrinfo() -> Result<()> {
|
||||
|
||||
let address_contact_bob_id = alice.add_or_lookup_address_contact_id(bob).await;
|
||||
let encrinfo = Contact::get_encrinfo(alice, address_contact_bob_id).await?;
|
||||
assert_eq!(encrinfo, "No encryption");
|
||||
assert_eq!(encrinfo, "No encryption.");
|
||||
|
||||
let contact = Contact::get_by_id(alice, address_contact_bob_id).await?;
|
||||
assert!(!contact.e2ee_avail(alice).await?);
|
||||
@@ -827,7 +832,7 @@ async fn test_contact_get_encrinfo() -> Result<()> {
|
||||
let encrinfo = Contact::get_encrinfo(alice, contact_bob_id).await?;
|
||||
assert_eq!(
|
||||
encrinfo,
|
||||
"End-to-end encryption available.
|
||||
"Messages are end-to-end encrypted.
|
||||
Fingerprints:
|
||||
|
||||
Me (alice@example.org):
|
||||
|
||||
127
src/context.rs
127
src/context.rs
@@ -8,7 +8,7 @@ use std::sync::atomic::AtomicBool;
|
||||
use std::sync::{Arc, OnceLock, Weak};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result, bail, ensure};
|
||||
use anyhow::{Result, bail, ensure};
|
||||
use async_channel::{self as channel, Receiver, Sender};
|
||||
use ratelimit::Ratelimit;
|
||||
use tokio::sync::{Mutex, Notify, RwLock};
|
||||
@@ -23,7 +23,6 @@ use crate::imap::{FolderMeaning, Imap, ServerMetadata};
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::log::warn;
|
||||
use crate::logged_debug_assert;
|
||||
use crate::login_param::EnteredLoginParam;
|
||||
use crate::message::{self, MessageState, MsgId};
|
||||
use crate::net::tls::TlsSessionStore;
|
||||
use crate::peer_channels::Iroh;
|
||||
@@ -37,6 +36,8 @@ use crate::tools::{self, duration_to_str, time, time_elapsed};
|
||||
use crate::transport::ConfiguredLoginParam;
|
||||
use crate::{chatlist_events, stats};
|
||||
|
||||
pub use crate::scheduler::connectivity::Connectivity;
|
||||
|
||||
/// Builder for the [`Context`].
|
||||
///
|
||||
/// Many arguments to the [`Context`] are kind of optional and only needed to handle
|
||||
@@ -244,9 +245,9 @@ pub struct InnerContext {
|
||||
pub(crate) scheduler: SchedulerState,
|
||||
pub(crate) ratelimit: RwLock<Ratelimit>,
|
||||
|
||||
/// Recently loaded quota information, if any.
|
||||
/// Set to `None` if quota was never tried to load.
|
||||
pub(crate) quota: RwLock<Option<QuotaInfo>>,
|
||||
/// Recently loaded quota information for each trasnport, if any.
|
||||
/// If quota was never tried to load, then the transport doesn't have an entry in the BTreeMap.
|
||||
pub(crate) quota: RwLock<BTreeMap<u32, QuotaInfo>>,
|
||||
|
||||
/// Notify about new messages.
|
||||
///
|
||||
@@ -352,7 +353,7 @@ pub fn get_info() -> BTreeMap<&'static str, String> {
|
||||
#[cfg(not(debug_assertions))]
|
||||
res.insert("debug_assertions", "Off".to_string());
|
||||
|
||||
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
|
||||
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());
|
||||
res.insert("num_cpus", num_cpus::get().to_string());
|
||||
@@ -480,7 +481,7 @@ impl Context {
|
||||
events,
|
||||
scheduler: SchedulerState::new(),
|
||||
ratelimit: RwLock::new(Ratelimit::new(Duration::new(3, 0), 3.0)), // Allow at least 1 message every second + a burst of 3.
|
||||
quota: RwLock::new(None),
|
||||
quota: RwLock::new(BTreeMap::new()),
|
||||
new_msgs_notify,
|
||||
server_id: RwLock::new(None),
|
||||
metadata: RwLock::new(None),
|
||||
@@ -615,8 +616,13 @@ impl Context {
|
||||
}
|
||||
|
||||
// Update quota (to send warning if full) - but only check it once in a while.
|
||||
// note: For now this only checks quota of primary transport,
|
||||
// because background check only checks primary transport at the moment
|
||||
if self
|
||||
.quota_needs_update(DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT)
|
||||
.quota_needs_update(
|
||||
session.transport_id(),
|
||||
DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT,
|
||||
)
|
||||
.await
|
||||
&& let Err(err) = self.update_recent_quota(&mut session).await
|
||||
{
|
||||
@@ -816,11 +822,6 @@ impl Context {
|
||||
|
||||
/// Returns information about the context as key-value pairs.
|
||||
pub async fn get_info(&self) -> Result<BTreeMap<&'static str, String>> {
|
||||
let l = EnteredLoginParam::load(self).await?;
|
||||
let l2 = ConfiguredLoginParam::load(self).await?.map_or_else(
|
||||
|| "Not configured".to_string(),
|
||||
|(_transport_id, param)| param.to_string(),
|
||||
);
|
||||
let secondary_addrs = self.get_secondary_self_addrs().await?.join(", ");
|
||||
let all_transports: Vec<String> = ConfiguredLoginParam::load_all(self)
|
||||
.await?
|
||||
@@ -879,10 +880,6 @@ impl Context {
|
||||
.get_config(Config::ConfiguredMvboxFolder)
|
||||
.await?
|
||||
.unwrap_or_else(|| "<unset>".to_string());
|
||||
let configured_trash_folder = self
|
||||
.get_config(Config::ConfiguredTrashFolder)
|
||||
.await?
|
||||
.unwrap_or_else(|| "<unset>".to_string());
|
||||
|
||||
let mut res = get_info();
|
||||
|
||||
@@ -910,8 +907,6 @@ impl Context {
|
||||
.unwrap_or_else(|| "<unset>".to_string()),
|
||||
);
|
||||
res.insert("proxy_enabled", proxy_enabled.to_string());
|
||||
res.insert("entered_account_settings", l.to_string());
|
||||
res.insert("used_account_settings", l2);
|
||||
res.insert("used_transport_settings", all_transports);
|
||||
|
||||
if let Some(server_id) = &*self.server_id.read().await {
|
||||
@@ -947,16 +942,14 @@ impl Context {
|
||||
}
|
||||
|
||||
res.insert("secondary_addrs", secondary_addrs);
|
||||
res.insert(
|
||||
"fetched_existing_msgs",
|
||||
self.get_config_bool(Config::FetchedExistingMsgs)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"show_emails",
|
||||
self.get_config_int(Config::ShowEmails).await?.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"who_can_call_me",
|
||||
self.get_config_int(Config::WhoCanCallMe).await?.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"download_limit",
|
||||
self.get_config_int(Config::DownloadLimit)
|
||||
@@ -971,7 +964,6 @@ impl Context {
|
||||
);
|
||||
res.insert("configured_inbox_folder", configured_inbox_folder);
|
||||
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("bcc_self", bcc_self.to_string());
|
||||
res.insert("sync_msgs", sync_msgs.to_string());
|
||||
@@ -995,12 +987,6 @@ impl Context {
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"delete_to_trash",
|
||||
self.get_config(Config::DeleteToTrash)
|
||||
.await?
|
||||
.unwrap_or_else(|| "<unset>".to_string()),
|
||||
);
|
||||
res.insert(
|
||||
"last_housekeeping",
|
||||
self.get_config_int(Config::LastHousekeeping)
|
||||
@@ -1091,13 +1077,6 @@ 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(),
|
||||
);
|
||||
res.insert(
|
||||
"std_header_protection_composing",
|
||||
self.sql
|
||||
@@ -1105,6 +1084,10 @@ impl Context {
|
||||
.await?
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
res.insert(
|
||||
"team_profile",
|
||||
self.get_config_bool(Config::TeamProfile).await?.to_string(),
|
||||
);
|
||||
|
||||
let elapsed = time_elapsed(&self.creation_time);
|
||||
res.insert("uptime", duration_to_str(elapsed));
|
||||
@@ -1122,21 +1105,19 @@ impl Context {
|
||||
let list = self
|
||||
.sql
|
||||
.query_map_vec(
|
||||
concat!(
|
||||
"SELECT m.id",
|
||||
" FROM msgs m",
|
||||
" LEFT JOIN contacts ct",
|
||||
" ON m.from_id=ct.id",
|
||||
" LEFT JOIN chats c",
|
||||
" ON m.chat_id=c.id",
|
||||
" WHERE m.state=?",
|
||||
" AND m.hidden=0",
|
||||
" AND m.chat_id>9",
|
||||
" AND ct.blocked=0",
|
||||
" AND c.blocked=0",
|
||||
" AND NOT(c.muted_until=-1 OR c.muted_until>?)",
|
||||
" ORDER BY m.timestamp DESC,m.id DESC;"
|
||||
),
|
||||
"SELECT m.id
|
||||
FROM msgs m
|
||||
LEFT JOIN contacts ct
|
||||
ON m.from_id=ct.id
|
||||
LEFT JOIN chats c
|
||||
ON m.chat_id=c.id
|
||||
WHERE m.state=?
|
||||
AND m.hidden=0
|
||||
AND m.chat_id>9
|
||||
AND ct.blocked=0
|
||||
AND c.blocked=0
|
||||
AND NOT(c.muted_until=-1 OR c.muted_until>?)
|
||||
ORDER BY m.timestamp DESC,m.id DESC",
|
||||
(MessageState::InFresh, time()),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
@@ -1288,45 +1269,12 @@ impl Context {
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
/// Returns true if given folder name is the name of the inbox.
|
||||
pub async fn is_inbox(&self, folder_name: &str) -> Result<bool> {
|
||||
let inbox = self.get_config(Config::ConfiguredInboxFolder).await?;
|
||||
Ok(inbox.as_deref() == Some(folder_name))
|
||||
}
|
||||
|
||||
/// Returns true if given folder name is the name of the "DeltaChat" folder.
|
||||
pub async fn is_mvbox(&self, folder_name: &str) -> Result<bool> {
|
||||
let mvbox = self.get_config(Config::ConfiguredMvboxFolder).await?;
|
||||
Ok(mvbox.as_deref() == Some(folder_name))
|
||||
}
|
||||
|
||||
/// Returns true if given folder name is the name of the trash folder.
|
||||
pub async fn is_trash(&self, folder_name: &str) -> Result<bool> {
|
||||
let trash = self.get_config(Config::ConfiguredTrashFolder).await?;
|
||||
Ok(trash.as_deref() == Some(folder_name))
|
||||
}
|
||||
|
||||
pub(crate) async fn should_delete_to_trash(&self) -> Result<bool> {
|
||||
if let Some(v) = self.get_config_bool_opt(Config::DeleteToTrash).await? {
|
||||
return Ok(v);
|
||||
}
|
||||
if let Some(provider) = self.get_configured_provider().await? {
|
||||
return Ok(provider.opt.delete_to_trash);
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Returns `target` for deleted messages as per `imap` table. Empty string means "delete w/o
|
||||
/// moving to trash".
|
||||
pub(crate) async fn get_delete_msgs_target(&self) -> Result<String> {
|
||||
if !self.should_delete_to_trash().await? {
|
||||
return Ok("".into());
|
||||
}
|
||||
self.get_config(Config::ConfiguredTrashFolder)
|
||||
.await?
|
||||
.context("No configured trash folder")
|
||||
}
|
||||
|
||||
pub(crate) fn derive_blobdir(dbfile: &Path) -> PathBuf {
|
||||
let mut blob_fname = OsString::new();
|
||||
blob_fname.push(dbfile.file_name().unwrap_or_default());
|
||||
@@ -1342,10 +1290,5 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns core version as a string.
|
||||
pub fn get_version_str() -> &'static str {
|
||||
&DC_VERSION_STR
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod context_tests;
|
||||
|
||||
@@ -297,6 +297,7 @@ async fn test_get_info_completeness() {
|
||||
"encrypted_device_token",
|
||||
"stats_last_update",
|
||||
"stats_last_old_contact_id",
|
||||
"simulate_receive_imf_error", // only used in tests
|
||||
];
|
||||
let t = TestContext::new().await;
|
||||
let info = t.get_info().await.unwrap();
|
||||
|
||||
532
src/download.rs
532
src/download.rs
@@ -1,27 +1,19 @@
|
||||
//! # Download large messages manually.
|
||||
|
||||
use std::cmp::max;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use anyhow::{Result, anyhow, bail, ensure};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::imap::session::Session;
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, Part};
|
||||
use crate::tools::time;
|
||||
use crate::{EventType, chatlist_events, stock_str};
|
||||
use crate::log::warn;
|
||||
use crate::message::{self, Message, MsgId, rfc724_mid_exists};
|
||||
use crate::{EventType, chatlist_events};
|
||||
|
||||
/// Download limits should not be used below `MIN_DOWNLOAD_LIMIT`.
|
||||
///
|
||||
/// For better UX, some messages as add-member, non-delivery-reports (NDN) or read-receipts (MDN)
|
||||
/// should always be downloaded completely to handle them correctly,
|
||||
/// also in larger groups and if group and contact avatar are attached.
|
||||
/// Most of these cases are caught by `MIN_DOWNLOAD_LIMIT`.
|
||||
pub(crate) const MIN_DOWNLOAD_LIMIT: u32 = 163840;
|
||||
pub(crate) mod post_msg_metadata;
|
||||
pub(crate) use post_msg_metadata::PostMsgMetadata;
|
||||
|
||||
/// If a message is downloaded only partially
|
||||
/// and `delete_server_after` is set to small timeouts (eg. "at once"),
|
||||
@@ -29,6 +21,16 @@ pub(crate) const MIN_DOWNLOAD_LIMIT: u32 = 163840;
|
||||
/// `MIN_DELETE_SERVER_AFTER` increases the timeout in this case.
|
||||
pub(crate) const MIN_DELETE_SERVER_AFTER: i64 = 48 * 60 * 60;
|
||||
|
||||
/// From this point onward outgoing messages are considered large
|
||||
/// and get a Pre-Message, which announces the Post-Message.
|
||||
/// This is only about sending so we can modify it any time.
|
||||
/// Current value is a bit less than the minimum auto-download setting from the UIs (which is 160
|
||||
/// KiB).
|
||||
pub(crate) const PRE_MSG_ATTACHMENT_SIZE_THRESHOLD: u64 = 140_000;
|
||||
|
||||
/// Max size for pre messages. A warning is emitted when this is exceeded.
|
||||
pub(crate) const PRE_MSG_SIZE_WARNING_THRESHOLD: usize = 150_000;
|
||||
|
||||
/// Download state of the message.
|
||||
#[derive(
|
||||
Debug,
|
||||
@@ -64,20 +66,8 @@ pub enum DownloadState {
|
||||
InProgress = 1000,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
// Returns validated download limit or `None` for "no limit".
|
||||
pub(crate) async fn download_limit(&self) -> Result<Option<u32>> {
|
||||
let download_limit = self.get_config_int(Config::DownloadLimit).await?;
|
||||
if download_limit <= 0 {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(max(MIN_DOWNLOAD_LIMIT, download_limit as u32)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MsgId {
|
||||
/// Schedules full message download for partially downloaded message.
|
||||
/// Schedules Post-Message download for partially downloaded message.
|
||||
pub async fn download_full(self, context: &Context) -> Result<()> {
|
||||
let msg = Message::load_from_db(context, self).await?;
|
||||
match msg.download_state() {
|
||||
@@ -86,11 +76,22 @@ impl MsgId {
|
||||
}
|
||||
DownloadState::InProgress => return Err(anyhow!("Download already in progress.")),
|
||||
DownloadState::Available | DownloadState::Failure => {
|
||||
if msg.rfc724_mid().is_empty() {
|
||||
return Err(anyhow!("Download not possible, message has no rfc724_mid"));
|
||||
}
|
||||
self.update_download_state(context, DownloadState::InProgress)
|
||||
.await?;
|
||||
info!(
|
||||
context,
|
||||
"Requesting full download of {:?}.",
|
||||
msg.rfc724_mid()
|
||||
);
|
||||
context
|
||||
.sql
|
||||
.execute("INSERT INTO download (msg_id) VALUES (?)", (self,))
|
||||
.execute(
|
||||
"INSERT INTO download (rfc724_mid, msg_id) VALUES (?,?)",
|
||||
(msg.rfc724_mid(), msg.id),
|
||||
)
|
||||
.await?;
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
}
|
||||
@@ -98,7 +99,8 @@ impl MsgId {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates the message download state. Returns `Ok` if the message doesn't exist anymore.
|
||||
/// Updates the message download state. Returns `Ok` if the message doesn't exist anymore or has
|
||||
/// the download state up to date.
|
||||
pub(crate) async fn update_download_state(
|
||||
self,
|
||||
context: &Context,
|
||||
@@ -107,7 +109,7 @@ impl MsgId {
|
||||
if context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET download_state=? WHERE id=?;",
|
||||
"UPDATE msgs SET download_state=? WHERE id=? AND download_state<>?1",
|
||||
(download_state, self),
|
||||
)
|
||||
.await?
|
||||
@@ -134,47 +136,46 @@ impl Message {
|
||||
}
|
||||
}
|
||||
|
||||
/// Actually download a message partially downloaded before.
|
||||
/// Actually downloads a message partially downloaded before if the message is available on the
|
||||
/// session transport, in which case returns `Some`. If the message is available on another
|
||||
/// transport, returns `None`.
|
||||
///
|
||||
/// Most messages are downloaded automatically on fetch instead.
|
||||
pub(crate) async fn download_msg(
|
||||
context: &Context,
|
||||
msg_id: MsgId,
|
||||
rfc724_mid: String,
|
||||
session: &mut Session,
|
||||
) -> Result<()> {
|
||||
let Some(msg) = Message::load_from_db_optional(context, msg_id).await? else {
|
||||
// If partially downloaded message was already deleted
|
||||
// we do not know its Message-ID anymore
|
||||
// so cannot download it.
|
||||
//
|
||||
// Probably the message expired due to `delete_device_after`
|
||||
// setting or was otherwise removed from the device,
|
||||
// so we don't want it to reappear anyway.
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
) -> Result<Option<()>> {
|
||||
let transport_id = session.transport_id();
|
||||
let row = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT uid, folder FROM imap WHERE rfc724_mid=? AND target!=''",
|
||||
(&msg.rfc724_mid,),
|
||||
"SELECT uid, folder, transport_id FROM imap
|
||||
WHERE rfc724_mid=? AND target!=''
|
||||
ORDER BY transport_id=? DESC LIMIT 1",
|
||||
(&rfc724_mid, transport_id),
|
||||
|row| {
|
||||
let server_uid: u32 = row.get(0)?;
|
||||
let server_folder: String = row.get(1)?;
|
||||
Ok((server_uid, server_folder))
|
||||
let msg_transport_id: u32 = row.get(2)?;
|
||||
Ok((server_uid, server_folder, msg_transport_id))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let Some((server_uid, server_folder)) = row else {
|
||||
let Some((server_uid, server_folder, msg_transport_id)) = row else {
|
||||
// No IMAP record found, we don't know the UID and folder.
|
||||
return Err(anyhow!("Call download_full() again to try over."));
|
||||
return Err(anyhow!(
|
||||
"IMAP location for {rfc724_mid:?} post-message is unknown"
|
||||
));
|
||||
};
|
||||
|
||||
if msg_transport_id != transport_id {
|
||||
return Ok(None);
|
||||
}
|
||||
session
|
||||
.fetch_single_msg(context, &server_folder, server_uid, msg.rfc724_mid.clone())
|
||||
.fetch_single_msg(context, &server_folder, server_uid, rfc724_mid)
|
||||
.await?;
|
||||
Ok(())
|
||||
Ok(Some(()))
|
||||
}
|
||||
|
||||
impl Session {
|
||||
@@ -193,10 +194,7 @@ impl Session {
|
||||
bail!("Attempt to fetch UID 0");
|
||||
}
|
||||
|
||||
let create = false;
|
||||
let folder_exists = self
|
||||
.select_with_uidvalidity(context, folder, create)
|
||||
.await?;
|
||||
let folder_exists = self.select_with_uidvalidity(context, folder).await?;
|
||||
ensure!(folder_exists, "No folder {folder}");
|
||||
|
||||
// we are connected, and the folder is selected
|
||||
@@ -205,7 +203,7 @@ impl Session {
|
||||
let mut uid_message_ids: BTreeMap<u32, String> = BTreeMap::new();
|
||||
uid_message_ids.insert(uid, rfc724_mid);
|
||||
let (sender, receiver) = async_channel::unbounded();
|
||||
self.fetch_many_msgs(context, folder, vec![uid], &uid_message_ids, false, sender)
|
||||
self.fetch_many_msgs(context, folder, vec![uid], &uid_message_ids, sender)
|
||||
.await?;
|
||||
if receiver.recv().await.is_err() {
|
||||
bail!("Failed to fetch UID {uid}");
|
||||
@@ -214,41 +212,139 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
impl MimeMessage {
|
||||
/// Creates a placeholder part and add that to `parts`.
|
||||
///
|
||||
/// To create the placeholder, only the outermost header can be used,
|
||||
/// the mime-structure itself is not available.
|
||||
///
|
||||
/// The placeholder part currently contains a text with size and availability of the message.
|
||||
pub(crate) async fn create_stub_from_partial_download(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
org_bytes: u32,
|
||||
) -> Result<()> {
|
||||
let mut text = format!(
|
||||
"[{}]",
|
||||
stock_str::partial_download_msg_body(context, org_bytes).await
|
||||
);
|
||||
if let Some(delete_server_after) = context.get_config_delete_server_after().await? {
|
||||
let until = stock_str::download_availability(
|
||||
context,
|
||||
time() + max(delete_server_after, MIN_DELETE_SERVER_AFTER),
|
||||
)
|
||||
.await;
|
||||
text += format!(" [{until}]").as_str();
|
||||
};
|
||||
|
||||
info!(context, "Partial download: {}", text);
|
||||
|
||||
self.do_add_single_part(Part {
|
||||
typ: Viewtype::Text,
|
||||
msg: text,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
Ok(())
|
||||
async fn set_state_to_failure(context: &Context, rfc724_mid: &str) -> Result<()> {
|
||||
if let Some(msg_id) = rfc724_mid_exists(context, rfc724_mid).await? {
|
||||
// Update download state to failure
|
||||
// so it can be retried.
|
||||
//
|
||||
// On success update_download_state() is not needed
|
||||
// as receive_imf() already
|
||||
// set the state and emitted the event.
|
||||
msg_id
|
||||
.update_download_state(context, DownloadState::Failure)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn available_post_msgs_contains_rfc724_mid(
|
||||
context: &Context,
|
||||
rfc724_mid: &str,
|
||||
) -> Result<bool> {
|
||||
Ok(context
|
||||
.sql
|
||||
.query_get_value::<String>(
|
||||
"SELECT rfc724_mid FROM available_post_msgs WHERE rfc724_mid=?",
|
||||
(&rfc724_mid,),
|
||||
)
|
||||
.await?
|
||||
.is_some())
|
||||
}
|
||||
|
||||
async fn delete_from_available_post_msgs(context: &Context, rfc724_mid: &str) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM available_post_msgs WHERE rfc724_mid=?",
|
||||
(&rfc724_mid,),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_from_downloads(context: &Context, rfc724_mid: &str) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM download WHERE rfc724_mid=?", (&rfc724_mid,))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn msg_is_downloaded_for(context: &Context, rfc724_mid: &str) -> Result<bool> {
|
||||
Ok(message::rfc724_mid_exists(context, rfc724_mid)
|
||||
.await?
|
||||
.is_some())
|
||||
}
|
||||
|
||||
pub(crate) async fn download_msgs(context: &Context, session: &mut Session) -> Result<()> {
|
||||
let rfc724_mids = context
|
||||
.sql
|
||||
.query_map_vec("SELECT rfc724_mid FROM download", (), |row| {
|
||||
let rfc724_mid: String = row.get(0)?;
|
||||
Ok(rfc724_mid)
|
||||
})
|
||||
.await?;
|
||||
|
||||
for rfc724_mid in &rfc724_mids {
|
||||
let res = download_msg(context, rfc724_mid.clone(), session).await;
|
||||
if let Ok(Some(())) = res {
|
||||
delete_from_downloads(context, rfc724_mid).await?;
|
||||
delete_from_available_post_msgs(context, rfc724_mid).await?;
|
||||
}
|
||||
if let Err(err) = res {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to download message rfc724_mid={rfc724_mid}: {:#}.", err
|
||||
);
|
||||
if !msg_is_downloaded_for(context, rfc724_mid).await? {
|
||||
// This is probably a classical email that vanished before we could download it
|
||||
warn!(
|
||||
context,
|
||||
"{rfc724_mid} download failed and there is no downloaded pre-message."
|
||||
);
|
||||
delete_from_downloads(context, rfc724_mid).await?;
|
||||
} else if available_post_msgs_contains_rfc724_mid(context, rfc724_mid).await? {
|
||||
warn!(
|
||||
context,
|
||||
"{rfc724_mid} is in available_post_msgs table but we failed to fetch it,
|
||||
so set the message to DownloadState::Failure - probably it was deleted on the server in the meantime"
|
||||
);
|
||||
set_state_to_failure(context, rfc724_mid).await?;
|
||||
delete_from_downloads(context, rfc724_mid).await?;
|
||||
delete_from_available_post_msgs(context, rfc724_mid).await?;
|
||||
} else {
|
||||
// leave the message in DownloadState::InProgress;
|
||||
// it will be downloaded once it arrives.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Downloads known post-messages without pre-messages
|
||||
/// in order to guard against lost pre-messages.
|
||||
pub(crate) async fn download_known_post_messages_without_pre_message(
|
||||
context: &Context,
|
||||
session: &mut Session,
|
||||
) -> Result<()> {
|
||||
let rfc724_mids = context
|
||||
.sql
|
||||
.query_map_vec("SELECT rfc724_mid FROM available_post_msgs", (), |row| {
|
||||
let rfc724_mid: String = row.get(0)?;
|
||||
Ok(rfc724_mid)
|
||||
})
|
||||
.await?;
|
||||
for rfc724_mid in &rfc724_mids {
|
||||
if !msg_is_downloaded_for(context, rfc724_mid).await? {
|
||||
// Download the Post-Message unconditionally,
|
||||
// because the Pre-Message got lost.
|
||||
// The message may be in the wrong order,
|
||||
// but at least we have it at all.
|
||||
let res = download_msg(context, rfc724_mid.clone(), session).await;
|
||||
if let Ok(Some(())) = res {
|
||||
delete_from_available_post_msgs(context, rfc724_mid).await?;
|
||||
}
|
||||
if let Err(err) = res {
|
||||
warn!(
|
||||
context,
|
||||
"download_known_post_messages_without_pre_message: Failed to download message rfc724_mid={rfc724_mid}: {:#}.",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -256,11 +352,8 @@ mod tests {
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
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::{E2EE_INFO_MSGS, TestContext, TestContextManager};
|
||||
use crate::chat::send_msg;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[test]
|
||||
fn test_downloadstate_values() {
|
||||
@@ -278,29 +371,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_download_limit() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
assert_eq!(t.download_limit().await?, None);
|
||||
|
||||
t.set_config(Config::DownloadLimit, Some("200000")).await?;
|
||||
assert_eq!(t.download_limit().await?, Some(200000));
|
||||
|
||||
t.set_config(Config::DownloadLimit, Some("20000")).await?;
|
||||
assert_eq!(t.download_limit().await?, Some(MIN_DOWNLOAD_LIMIT));
|
||||
|
||||
t.set_config(Config::DownloadLimit, None).await?;
|
||||
assert_eq!(t.download_limit().await?, None);
|
||||
|
||||
for val in &["0", "-1", "-100", "", "foo"] {
|
||||
t.set_config(Config::DownloadLimit, Some(val)).await?;
|
||||
assert_eq!(t.download_limit().await?, None);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_update_download_state() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
@@ -332,230 +402,4 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_partial_receive_imf() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
let header = "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: bob@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <Mr.12345678901@example.com>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\
|
||||
Content-Type: text/plain";
|
||||
|
||||
receive_imf_from_inbox(
|
||||
&t,
|
||||
"Mr.12345678901@example.com",
|
||||
header.as_bytes(),
|
||||
false,
|
||||
Some(100000),
|
||||
)
|
||||
.await?;
|
||||
let msg = t.get_last_msg().await;
|
||||
assert_eq!(msg.download_state(), DownloadState::Available);
|
||||
assert_eq!(msg.get_subject(), "foo");
|
||||
assert!(
|
||||
msg.get_text()
|
||||
.contains(&stock_str::partial_download_msg_body(&t, 100000).await)
|
||||
);
|
||||
|
||||
receive_imf_from_inbox(
|
||||
&t,
|
||||
"Mr.12345678901@example.com",
|
||||
format!("{header}\n\n100k text...").as_bytes(),
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let msg = t.get_last_msg().await;
|
||||
assert_eq!(msg.download_state(), DownloadState::Done);
|
||||
assert_eq!(msg.get_subject(), "foo");
|
||||
assert_eq!(msg.get_text(), "100k text...");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_partial_download_and_ephemeral() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = t
|
||||
.create_chat_with_contact("bob", "bob@example.org")
|
||||
.await
|
||||
.id;
|
||||
chat_id
|
||||
.set_ephemeral_timer(&t, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
|
||||
// download message from bob partially, this must not change the ephemeral timer
|
||||
receive_imf_from_inbox(
|
||||
&t,
|
||||
"first@example.org",
|
||||
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",
|
||||
false,
|
||||
Some(100000),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
chat_id.get_ephemeral_timer(&t).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_status_update_expands_to_nothing() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat_id = alice.create_chat(&bob).await.id;
|
||||
|
||||
let file = alice.get_blobdir().join("minimal.xdc");
|
||||
tokio::fs::write(&file, include_bytes!("../test-data/webxdc/minimal.xdc")).await?;
|
||||
let mut instance = Message::new(Viewtype::File);
|
||||
instance.set_file_and_deduplicate(&alice, &file, None, None)?;
|
||||
let _sent1 = alice.send_msg(chat_id, &mut instance).await;
|
||||
|
||||
alice
|
||||
.send_webxdc_status_update(instance.id, r#"{"payload":7}"#)
|
||||
.await?;
|
||||
alice.flush_status_updates().await?;
|
||||
let sent2 = alice.pop_sent_msg().await;
|
||||
let sent2_rfc724_mid = sent2.load_from_db().await.rfc724_mid;
|
||||
|
||||
// not downloading the status update results in an placeholder
|
||||
receive_imf_from_inbox(
|
||||
&bob,
|
||||
&sent2_rfc724_mid,
|
||||
sent2.payload().as_bytes(),
|
||||
false,
|
||||
Some(sent2.payload().len() as u32),
|
||||
)
|
||||
.await?;
|
||||
let msg = bob.get_last_msg().await;
|
||||
let chat_id = msg.chat_id;
|
||||
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
|
||||
// (usually status updates are too small for not being downloaded directly)
|
||||
receive_imf_from_inbox(
|
||||
&bob,
|
||||
&sent2_rfc724_mid,
|
||||
sent2.payload().as_bytes(),
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), E2EE_INFO_MSGS);
|
||||
assert!(
|
||||
Message::load_from_db_optional(&bob, msg.id)
|
||||
.await?
|
||||
.is_none()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_mdn_expands_to_nothing() -> Result<()> {
|
||||
let bob = TestContext::new_bob().await;
|
||||
let raw = b"Subject: Message opened\n\
|
||||
Date: Mon, 10 Jan 2020 00:00:00 +0000\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <bar@example.org>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
From: Bob <bob@example.org>\n\
|
||||
Content-Type: multipart/report; report-type=disposition-notification;\n\t\
|
||||
boundary=\"kJBbU58X1xeWNHgBtTbMk80M5qnV4N\"\n\
|
||||
\n\
|
||||
\n\
|
||||
--kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\
|
||||
Content-Type: text/plain; charset=utf-8\n\
|
||||
\n\
|
||||
bla\n\
|
||||
\n\
|
||||
\n\
|
||||
--kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\
|
||||
Content-Type: message/disposition-notification\n\
|
||||
\n\
|
||||
Reporting-UA: Delta Chat 1.88.0\n\
|
||||
Original-Recipient: rfc822;bob@example.org\n\
|
||||
Final-Recipient: rfc822;bob@example.org\n\
|
||||
Original-Message-ID: <foo@example.org>\n\
|
||||
Disposition: manual-action/MDN-sent-automatically; displayed\n\
|
||||
\n\
|
||||
\n\
|
||||
--kJBbU58X1xeWNHgBtTbMk80M5qnV4N--\n\
|
||||
";
|
||||
|
||||
// not downloading the mdn results in an placeholder
|
||||
receive_imf_from_inbox(&bob, "bar@example.org", raw, false, Some(raw.len() as u32)).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!(msg.download_state(), DownloadState::Available);
|
||||
|
||||
// downloading the mdn afterwards expands to nothing and deletes the placeholder directly
|
||||
// (usually mdn are too small for not being downloaded directly)
|
||||
receive_imf_from_inbox(&bob, "bar@example.org", raw, false, None).await?;
|
||||
assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 0);
|
||||
assert!(
|
||||
Message::load_from_db_optional(&bob, msg.id)
|
||||
.await?
|
||||
.is_none()
|
||||
);
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
251
src/download/post_msg_metadata.rs
Normal file
251
src/download/post_msg_metadata.rs
Normal file
@@ -0,0 +1,251 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use num_traits::ToPrimitive;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::log::warn;
|
||||
use crate::message::Message;
|
||||
use crate::message::Viewtype;
|
||||
use crate::param::{Param, Params};
|
||||
|
||||
/// Metadata contained in Pre-Message that describes the Post-Message.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PostMsgMetadata {
|
||||
/// size of the attachment in bytes
|
||||
pub(crate) size: u64,
|
||||
/// Real viewtype of message
|
||||
pub(crate) viewtype: Viewtype,
|
||||
/// the original file name
|
||||
pub(crate) filename: String,
|
||||
/// Width and height of the image or video
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) wh: Option<(i32, i32)>,
|
||||
/// Duration of audio file or video in milliseconds
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) duration: Option<i32>,
|
||||
}
|
||||
|
||||
impl PostMsgMetadata {
|
||||
/// Returns `PostMsgMetadata` for messages with file attachment and `None` otherwise.
|
||||
pub(crate) async fn from_msg(context: &Context, message: &Message) -> Result<Option<Self>> {
|
||||
if !message.viewtype.has_file() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let size = message
|
||||
.get_filebytes(context)
|
||||
.await?
|
||||
.context("Unexpected: file has no size")?;
|
||||
let filename = message
|
||||
.param
|
||||
.get(Param::Filename)
|
||||
.unwrap_or_default()
|
||||
.to_owned();
|
||||
let wh = {
|
||||
match (
|
||||
message.param.get_int(Param::Width),
|
||||
message.param.get_int(Param::Height),
|
||||
) {
|
||||
(None, None) => None,
|
||||
(Some(width), Some(height)) => Some((width, height)),
|
||||
wh => {
|
||||
warn!(
|
||||
context,
|
||||
"Message {} misses width or height: {:?}.", message.id, wh
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
let duration = message.param.get_int(Param::Duration);
|
||||
|
||||
Ok(Some(Self {
|
||||
size,
|
||||
filename,
|
||||
viewtype: message.viewtype,
|
||||
wh,
|
||||
duration,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn to_header_value(&self) -> Result<String> {
|
||||
Ok(serde_json::to_string(&self)?)
|
||||
}
|
||||
|
||||
pub(crate) fn try_from_header_value(value: &str) -> Result<Self> {
|
||||
Ok(serde_json::from_str(value)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Params {
|
||||
/// Applies data from post_msg_metadata to Params
|
||||
pub(crate) fn apply_post_msg_metadata(
|
||||
&mut self,
|
||||
post_msg_metadata: &PostMsgMetadata,
|
||||
) -> &mut Self {
|
||||
self.set(Param::PostMessageFileBytes, post_msg_metadata.size);
|
||||
if !post_msg_metadata.filename.is_empty() {
|
||||
self.set(Param::Filename, &post_msg_metadata.filename);
|
||||
}
|
||||
self.set_i64(
|
||||
Param::PostMessageViewtype,
|
||||
post_msg_metadata.viewtype.to_i64().unwrap_or_default(),
|
||||
);
|
||||
if let Some((width, height)) = post_msg_metadata.wh {
|
||||
self.set(Param::Width, width);
|
||||
self.set(Param::Height, height);
|
||||
}
|
||||
if let Some(duration) = post_msg_metadata.duration {
|
||||
self.set(Param::Duration, duration);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Result;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::{
|
||||
message::{Message, Viewtype},
|
||||
test_utils::{TestContextManager, create_test_image},
|
||||
};
|
||||
|
||||
use super::PostMsgMetadata;
|
||||
|
||||
/// Build from message with file attachment
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_build_from_file_msg() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
|
||||
let mut file_msg = Message::new(Viewtype::File);
|
||||
file_msg.set_file_from_bytes(alice, "test.bin", &vec![0u8; 1_000_000], None)?;
|
||||
let post_msg_metadata = PostMsgMetadata::from_msg(alice, &file_msg).await?;
|
||||
assert_eq!(
|
||||
post_msg_metadata,
|
||||
Some(PostMsgMetadata {
|
||||
size: 1_000_000,
|
||||
viewtype: Viewtype::File,
|
||||
filename: "test.bin".to_string(),
|
||||
wh: None,
|
||||
duration: None,
|
||||
})
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Build from message with image attachment
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_build_from_image_msg() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let mut image_msg = Message::new(Viewtype::Image);
|
||||
|
||||
let (width, height) = (1080, 1920);
|
||||
let test_img = create_test_image(width, height)?;
|
||||
image_msg.set_file_from_bytes(alice, "vacation.png", &test_img, None)?;
|
||||
// this is usually done while sending,
|
||||
// but we don't send it here, so we need to call it ourself
|
||||
image_msg.try_calc_and_set_dimensions(alice).await?;
|
||||
let post_msg_metadata = PostMsgMetadata::from_msg(alice, &image_msg).await?;
|
||||
assert_eq!(
|
||||
post_msg_metadata,
|
||||
Some(PostMsgMetadata {
|
||||
size: 1816098,
|
||||
viewtype: Viewtype::Image,
|
||||
filename: "vacation.png".to_string(),
|
||||
wh: Some((width as i32, height as i32)),
|
||||
duration: None,
|
||||
})
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that serialisation results in expected format
|
||||
#[test]
|
||||
fn test_serialize_to_header() -> Result<()> {
|
||||
assert_eq!(
|
||||
PostMsgMetadata {
|
||||
size: 1_000_000,
|
||||
viewtype: Viewtype::File,
|
||||
filename: "test.bin".to_string(),
|
||||
wh: None,
|
||||
duration: None,
|
||||
}
|
||||
.to_header_value()?,
|
||||
"{\"size\":1000000,\"viewtype\":\"File\",\"filename\":\"test.bin\"}"
|
||||
);
|
||||
assert_eq!(
|
||||
PostMsgMetadata {
|
||||
size: 5_342_765,
|
||||
viewtype: Viewtype::Image,
|
||||
filename: "vacation.png".to_string(),
|
||||
wh: Some((1080, 1920)),
|
||||
duration: None,
|
||||
}
|
||||
.to_header_value()?,
|
||||
"{\"size\":5342765,\"viewtype\":\"Image\",\"filename\":\"vacation.png\",\"wh\":[1080,1920]}"
|
||||
);
|
||||
assert_eq!(
|
||||
PostMsgMetadata {
|
||||
size: 5_000,
|
||||
viewtype: Viewtype::Audio,
|
||||
filename: "audio-DD-MM-YY.ogg".to_string(),
|
||||
wh: None,
|
||||
duration: Some(152_310),
|
||||
}
|
||||
.to_header_value()?,
|
||||
"{\"size\":5000,\"viewtype\":\"Audio\",\"filename\":\"audio-DD-MM-YY.ogg\",\"duration\":152310}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that deserialisation from expected format works
|
||||
/// This test will become important for compatibility between versions in the future
|
||||
#[test]
|
||||
fn test_deserialize_from_header() -> Result<()> {
|
||||
assert_eq!(
|
||||
serde_json::from_str::<PostMsgMetadata>(
|
||||
"{\"size\":1000000,\"viewtype\":\"File\",\"filename\":\"test.bin\",\"wh\":null,\"duration\":null}"
|
||||
)?,
|
||||
PostMsgMetadata {
|
||||
size: 1_000_000,
|
||||
viewtype: Viewtype::File,
|
||||
filename: "test.bin".to_string(),
|
||||
wh: None,
|
||||
duration: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<PostMsgMetadata>(
|
||||
"{\"size\":5342765,\"viewtype\":\"Image\",\"filename\":\"vacation.png\",\"wh\":[1080,1920]}"
|
||||
)?,
|
||||
PostMsgMetadata {
|
||||
size: 5_342_765,
|
||||
viewtype: Viewtype::Image,
|
||||
filename: "vacation.png".to_string(),
|
||||
wh: Some((1080, 1920)),
|
||||
duration: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<PostMsgMetadata>(
|
||||
"{\"size\":5000,\"viewtype\":\"Audio\",\"filename\":\"audio-DD-MM-YY.ogg\",\"duration\":152310}"
|
||||
)?,
|
||||
PostMsgMetadata {
|
||||
size: 5_000,
|
||||
viewtype: Viewtype::Audio,
|
||||
filename: "audio-DD-MM-YY.ogg".to_string(),
|
||||
wh: None,
|
||||
duration: Some(152_310),
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -474,8 +474,10 @@ pub(crate) async fn delete_expired_messages(context: &Context, now: i64) -> Resu
|
||||
// If you change which information is preserved here, also change `MsgId::trash()`
|
||||
// and other places it references.
|
||||
let mut del_msg_stmt = transaction.prepare(
|
||||
"INSERT OR REPLACE INTO msgs (id, rfc724_mid, timestamp, chat_id)
|
||||
SELECT ?1, rfc724_mid, timestamp, ? FROM msgs WHERE id=?1",
|
||||
"
|
||||
INSERT OR REPLACE INTO msgs (id, rfc724_mid, pre_rfc724_mid, timestamp, chat_id)
|
||||
SELECT ?1, rfc724_mid, pre_rfc724_mid, timestamp, ? FROM msgs WHERE id=?1
|
||||
",
|
||||
)?;
|
||||
let mut del_location_stmt =
|
||||
transaction.prepare("DELETE FROM locations WHERE independent=1 AND id=?")?;
|
||||
@@ -663,25 +665,19 @@ pub(crate) async fn delete_expired_imap_messages(context: &Context) -> Result<()
|
||||
now - max(delete_server_after, MIN_DELETE_SERVER_AFTER),
|
||||
),
|
||||
};
|
||||
let target = context.get_delete_msgs_target().await?;
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE imap
|
||||
SET target=?
|
||||
SET target=''
|
||||
WHERE rfc724_mid IN (
|
||||
SELECT rfc724_mid FROM msgs
|
||||
WHERE ((download_state = 0 AND timestamp < ?) OR
|
||||
(download_state != 0 AND timestamp < ?) OR
|
||||
(ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?))
|
||||
)",
|
||||
(
|
||||
&target,
|
||||
threshold_timestamp,
|
||||
threshold_timestamp_extended,
|
||||
now,
|
||||
),
|
||||
(threshold_timestamp, threshold_timestamp_extended, now),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -417,13 +417,13 @@ pub enum EventType {
|
||||
chat_id: ChatId,
|
||||
},
|
||||
|
||||
/// One or more transports has changed.
|
||||
/// One or more transports has changed or another transport is primary now.
|
||||
///
|
||||
/// This event is used for tests to detect when transport
|
||||
/// synchronization messages arrives.
|
||||
/// UIs don't need to use it, it is unlikely
|
||||
/// that user modifies transports on multiple
|
||||
/// devices simultaneously.
|
||||
/// UI should update the list.
|
||||
///
|
||||
/// This event is emitted when a transport
|
||||
/// synchronization message modifies transports,
|
||||
/// but not when the UI modifies the transport list by itself.
|
||||
TransportsModified,
|
||||
|
||||
/// Event for using in tests, e.g. as a fence between normally generated events.
|
||||
|
||||
@@ -91,6 +91,7 @@ pub enum HeaderDef {
|
||||
ChatDispositionNotificationTo,
|
||||
ChatWebrtcRoom,
|
||||
ChatWebrtcAccepted,
|
||||
ChatWebrtcHasVideoInitially,
|
||||
|
||||
/// This message deletes the messages listed in the value by rfc724_mid.
|
||||
ChatDelete,
|
||||
@@ -102,6 +103,21 @@ pub enum HeaderDef {
|
||||
/// used to encrypt and decrypt messages.
|
||||
/// This secret is sent to a new member in the member-addition message.
|
||||
ChatBroadcastSecret,
|
||||
/// A message with a large attachment is split into two messages:
|
||||
/// A pre-message, which contains everything but the attachment,
|
||||
/// and a Post-Message.
|
||||
/// The Pre-Message gets a `Chat-Post-Message-Id` header
|
||||
/// referencing the Post-Message's rfc724_mid.
|
||||
ChatPostMessageId,
|
||||
|
||||
/// Announces Post-Message metadata in a Pre-Message.
|
||||
/// Contains a serialized `PostMsgMetadata` struct.
|
||||
ChatPostMessageMetadata,
|
||||
|
||||
/// This message is preceded by a Pre-Message
|
||||
/// and thus this message can be skipped while fetching messages.
|
||||
/// This is an unprotected header.
|
||||
ChatIsPostMessage,
|
||||
|
||||
/// [Autocrypt](https://autocrypt.org/) header.
|
||||
Autocrypt,
|
||||
@@ -147,6 +163,9 @@ pub enum HeaderDef {
|
||||
|
||||
impl HeaderDef {
|
||||
/// Returns the corresponding header string.
|
||||
///
|
||||
/// Format is lower-kebab-case for easy comparisons.
|
||||
/// This method is used in message receiving and testing.
|
||||
pub fn get_headername(&self) -> &'static str {
|
||||
self.into()
|
||||
}
|
||||
|
||||
85
src/html.rs
85
src/html.rs
@@ -254,13 +254,20 @@ fn mimepart_to_data_url(mail: &mailparse::ParsedMail<'_>) -> Result<String> {
|
||||
|
||||
impl MsgId {
|
||||
/// Get HTML by database message id.
|
||||
/// This requires `mime_headers` field to be set for the message;
|
||||
/// this is the case at least when `Message.has_html()` returns true
|
||||
/// (we do not save raw mime unconditionally in the database to save space).
|
||||
/// Returns `Some` at least if `Message.has_html()` is true.
|
||||
/// NB: we do not save raw mime unconditionally in the database to save space.
|
||||
/// The corresponding ffi-function is `dc_get_msg_html()`.
|
||||
pub async fn get_html(self, context: &Context) -> Result<Option<String>> {
|
||||
let rawmime = message::get_mime_headers(context, self).await?;
|
||||
// If there are many concurrent db readers, going to the queue earlier makes sense.
|
||||
let (param, rawmime) = tokio::join!(
|
||||
self.get_param(context),
|
||||
message::get_mime_headers(context, self)
|
||||
);
|
||||
if let Some(html) = param?.get(SendHtml) {
|
||||
return Ok(Some(html.to_string()));
|
||||
}
|
||||
|
||||
let rawmime = rawmime?;
|
||||
if !rawmime.is_empty() {
|
||||
match HtmlMsgParser::from_bytes(context, &rawmime).await {
|
||||
Err(err) => {
|
||||
@@ -279,9 +286,9 @@ impl MsgId {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat;
|
||||
use crate::chat::{forward_msgs, save_msgs};
|
||||
use crate::chat::{self, Chat, forward_msgs, save_msgs};
|
||||
use crate::config::Config;
|
||||
use crate::constants;
|
||||
use crate::contact::ContactId;
|
||||
use crate::message::{MessengerMessage, Viewtype};
|
||||
use crate::receive_imf::receive_imf;
|
||||
@@ -440,7 +447,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_html_forwarding() {
|
||||
async fn test_html_forwarding() -> Result<()> {
|
||||
// alice receives a non-delta html-message
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
@@ -459,31 +466,57 @@ test some special html-characters as < > and & but also " and &#x
|
||||
assert!(html.contains("this is <b>html</b>"));
|
||||
|
||||
// alice: create chat with bob and forward received html-message there
|
||||
let chat = alice.create_chat_with_contact("", "bob@example.net").await;
|
||||
forward_msgs(alice, &[msg.get_id()], chat.get_id())
|
||||
let chat_alice = alice.create_chat_with_contact("", "bob@example.net").await;
|
||||
forward_msgs(alice, &[msg.get_id()], chat_alice.get_id())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = alice.get_last_msg_in(chat.get_id()).await;
|
||||
assert_eq!(msg.get_from_id(), ContactId::SELF);
|
||||
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
|
||||
assert!(msg.is_forwarded());
|
||||
assert!(msg.get_text().contains("this is plain"));
|
||||
assert!(msg.has_html());
|
||||
let html = msg.get_id().get_html(alice).await.unwrap().unwrap();
|
||||
assert!(html.contains("this is <b>html</b>"));
|
||||
async fn check_sender(ctx: &TestContext, chat: &Chat) {
|
||||
let msg = ctx.get_last_msg_in(chat.get_id()).await;
|
||||
assert_eq!(msg.get_from_id(), ContactId::SELF);
|
||||
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
|
||||
assert!(msg.is_forwarded());
|
||||
assert!(msg.get_text().contains("this is plain"));
|
||||
assert!(msg.has_html());
|
||||
let html = msg.get_id().get_html(ctx).await.unwrap().unwrap();
|
||||
assert!(html.contains("this is <b>html</b>"));
|
||||
}
|
||||
check_sender(alice, &chat_alice).await;
|
||||
|
||||
// bob: check that bob also got the html-part of the forwarded message
|
||||
let bob = &tcm.bob().await;
|
||||
let chat = bob.create_chat_with_contact("", "alice@example.org").await;
|
||||
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(chat.id, msg.chat_id);
|
||||
assert_ne!(msg.get_from_id(), ContactId::SELF);
|
||||
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
|
||||
assert!(msg.is_forwarded());
|
||||
assert!(msg.get_text().contains("this is plain"));
|
||||
let chat_bob = bob.create_chat_with_contact("", "alice@example.org").await;
|
||||
async fn check_receiver(ctx: &TestContext, chat: &Chat, sender: &TestContext) {
|
||||
let msg = ctx.recv_msg(&sender.pop_sent_msg().await).await;
|
||||
assert_eq!(chat.id, msg.chat_id);
|
||||
assert_ne!(msg.get_from_id(), ContactId::SELF);
|
||||
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
|
||||
assert!(msg.is_forwarded());
|
||||
assert!(msg.get_text().contains("this is plain"));
|
||||
assert!(msg.has_html());
|
||||
let html = msg.get_id().get_html(ctx).await.unwrap().unwrap();
|
||||
assert!(html.contains("this is <b>html</b>"));
|
||||
}
|
||||
check_receiver(bob, &chat_bob, alice).await;
|
||||
|
||||
// Let's say that the alice and bob profiles are on the same device,
|
||||
// so alice can forward the message to herself via bob profile!
|
||||
chat::forward_msgs_2ctx(alice, &[msg.get_id()], bob, chat_bob.get_id()).await?;
|
||||
check_sender(bob, &chat_bob).await;
|
||||
check_receiver(alice, &chat_alice, bob).await;
|
||||
|
||||
// Check cross-profile forwarding of long outgoing messages.
|
||||
let line = "this text with 42 chars is just repeated.\n";
|
||||
let long_txt = line.repeat(constants::DC_DESIRED_TEXT_LEN / line.len() + 2);
|
||||
let mut msg = Message::new_text(long_txt);
|
||||
alice.send_msg(chat_alice.id, &mut msg).await;
|
||||
let msg = alice.get_last_msg_in(chat_alice.id).await;
|
||||
assert!(msg.has_html());
|
||||
let html = msg.get_id().get_html(bob).await.unwrap().unwrap();
|
||||
assert!(html.contains("this is <b>html</b>"));
|
||||
let html = msg.id.get_html(alice).await?.unwrap();
|
||||
chat::forward_msgs_2ctx(alice, &[msg.get_id()], bob, chat_bob.get_id()).await?;
|
||||
let msg = bob.get_last_msg_in(chat_bob.id).await;
|
||||
assert!(msg.has_html());
|
||||
assert_eq!(msg.id.get_html(bob).await?.unwrap(), html);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
575
src/imap.rs
575
src/imap.rs
@@ -16,19 +16,20 @@ use std::{
|
||||
use anyhow::{Context as _, Result, bail, ensure, format_err};
|
||||
use async_channel::{self, Receiver, Sender};
|
||||
use async_imap::types::{Fetch, Flag, Name, NameAttribute, UnsolicitedResponse};
|
||||
use deltachat_contact_tools::ContactAddress;
|
||||
use futures::{FutureExt as _, TryStreamExt};
|
||||
use futures_lite::FutureExt;
|
||||
use num_traits::FromPrimitive;
|
||||
use ratelimit::Ratelimit;
|
||||
use url::Url;
|
||||
|
||||
use crate::calls::{create_fallback_ice_servers, create_ice_servers_from_metadata};
|
||||
use crate::calls::{
|
||||
UnresolvedIceServer, create_fallback_ice_servers, create_ice_servers_from_metadata,
|
||||
};
|
||||
use crate::chat::{self, ChatId, ChatIdBlocked, add_device_msg};
|
||||
use crate::chatlist_events;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{self, Blocked, Chattype, ShowEmails};
|
||||
use crate::contact::{Contact, ContactId, Modifier, Origin};
|
||||
use crate::constants::{self, Blocked, DC_VERSION_STR, ShowEmails};
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
@@ -57,7 +58,6 @@ pub mod select_folder;
|
||||
pub(crate) mod session;
|
||||
|
||||
use client::{Client, determine_capabilities};
|
||||
use mailparse::SingleInfo;
|
||||
use session::Session;
|
||||
|
||||
pub(crate) const GENERATED_PREFIX: &str = "GEN_";
|
||||
@@ -67,7 +67,6 @@ const RFC724MID_UID: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
|
||||
X-MICROSOFT-ORIGINAL-MESSAGE-ID\
|
||||
)])";
|
||||
const BODY_FULL: &str = "(FLAGS BODY.PEEK[])";
|
||||
const BODY_PARTIAL: &str = "(FLAGS RFC822.SIZE BODY.PEEK[HEADER])";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Imap {
|
||||
@@ -123,7 +122,7 @@ struct OAuth2 {
|
||||
access_token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct ServerMetadata {
|
||||
/// IMAP METADATA `/shared/comment` as defined in
|
||||
/// <https://www.rfc-editor.org/rfc/rfc5464#section-6.2.1>.
|
||||
@@ -135,16 +134,15 @@ pub(crate) struct ServerMetadata {
|
||||
|
||||
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,
|
||||
/// ICE servers for WebRTC calls.
|
||||
pub ice_servers: Vec<UnresolvedIceServer>,
|
||||
|
||||
/// Timestamp when ICE servers are considered
|
||||
/// expired and should be updated.
|
||||
///
|
||||
/// If ICE servers are about to expire, new TURN credentials
|
||||
/// should be fetched from the server
|
||||
/// to be ready for WebRTC calls.
|
||||
pub ice_servers_expiration_timestamp: i64,
|
||||
}
|
||||
|
||||
@@ -185,7 +183,7 @@ impl FolderMeaning {
|
||||
FolderMeaning::Spam => None,
|
||||
FolderMeaning::Inbox => Some(Config::ConfiguredInboxFolder),
|
||||
FolderMeaning::Mvbox => Some(Config::ConfiguredMvboxFolder),
|
||||
FolderMeaning::Trash => Some(Config::ConfiguredTrashFolder),
|
||||
FolderMeaning::Trash => None,
|
||||
FolderMeaning::Virtual => None,
|
||||
}
|
||||
}
|
||||
@@ -501,13 +499,7 @@ impl Imap {
|
||||
.get_raw_config_int(constants::DC_FOLDERS_CONFIGURED_KEY)
|
||||
.await?;
|
||||
if folders_configured.unwrap_or_default() < constants::DC_FOLDERS_CONFIGURED_VERSION {
|
||||
let is_chatmail = match context.get_config_bool(Config::FixIsChatmail).await? {
|
||||
false => session.is_chatmail(),
|
||||
true => context.get_config_bool(Config::IsChatmail).await?,
|
||||
};
|
||||
let create_mvbox = !is_chatmail || context.get_config_bool(Config::MvboxMove).await?;
|
||||
self.configure_folders(context, &mut session, create_mvbox)
|
||||
.await?;
|
||||
self.configure_folders(context, &mut session).await?;
|
||||
}
|
||||
|
||||
Ok(session)
|
||||
@@ -565,9 +557,8 @@ impl Imap {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let create = false;
|
||||
let folder_exists = session
|
||||
.select_with_uidvalidity(context, folder, create)
|
||||
.select_with_uidvalidity(context, folder)
|
||||
.await
|
||||
.with_context(|| format!("Failed to select folder {folder:?}"))?;
|
||||
if !folder_exists {
|
||||
@@ -615,11 +606,16 @@ impl Imap {
|
||||
.context("prefetch")?;
|
||||
let read_cnt = msgs.len();
|
||||
|
||||
let download_limit = context.download_limit().await?;
|
||||
let mut uids_fetch = Vec::<(u32, bool /* partially? */)>::with_capacity(msgs.len() + 1);
|
||||
let mut uids_fetch: Vec<u32> = Vec::new();
|
||||
let mut available_post_msgs: Vec<String> = Vec::new();
|
||||
let mut download_later: Vec<String> = Vec::new();
|
||||
let mut uid_message_ids = BTreeMap::new();
|
||||
let mut largest_uid_skipped = None;
|
||||
let delete_target = context.get_delete_msgs_target().await?;
|
||||
|
||||
let download_limit: Option<u32> = context
|
||||
.get_config_parsed(Config::DownloadLimit)
|
||||
.await?
|
||||
.filter(|&l| 0 < l);
|
||||
|
||||
// Store the info about IMAP messages in the database.
|
||||
for (uid, ref fetch_response) in msgs {
|
||||
@@ -632,6 +628,9 @@ impl Imap {
|
||||
};
|
||||
|
||||
let message_id = prefetch_get_message_id(&headers);
|
||||
let size = fetch_response
|
||||
.size
|
||||
.context("imap fetch response does not contain size")?;
|
||||
|
||||
// Determine the target folder where the message should be moved to.
|
||||
//
|
||||
@@ -661,7 +660,7 @@ impl Imap {
|
||||
|
||||
let _target;
|
||||
let target = if delete {
|
||||
&delete_target
|
||||
""
|
||||
} else {
|
||||
_target = target_folder(context, folder, folder_meaning, &headers).await?;
|
||||
&_target
|
||||
@@ -706,14 +705,27 @@ impl Imap {
|
||||
)
|
||||
.await.context("prefetch_should_download")?
|
||||
{
|
||||
match download_limit {
|
||||
Some(download_limit) => uids_fetch.push((
|
||||
uid,
|
||||
fetch_response.size.unwrap_or_default() > download_limit,
|
||||
)),
|
||||
None => uids_fetch.push((uid, false)),
|
||||
}
|
||||
uid_message_ids.insert(uid, message_id);
|
||||
if headers
|
||||
.get_header_value(HeaderDef::ChatIsPostMessage)
|
||||
.is_some()
|
||||
{
|
||||
info!(context, "{message_id:?} is a post-message.");
|
||||
available_post_msgs.push(message_id.clone());
|
||||
|
||||
if download_limit.is_none_or(|download_limit| size <= download_limit) {
|
||||
download_later.push(message_id.clone());
|
||||
}
|
||||
largest_uid_skipped = Some(uid);
|
||||
} else {
|
||||
info!(context, "{message_id:?} is not a post-message.");
|
||||
if download_limit.is_none_or(|download_limit| size <= download_limit) {
|
||||
uids_fetch.push(uid);
|
||||
uid_message_ids.insert(uid, message_id);
|
||||
} else {
|
||||
download_later.push(message_id.clone());
|
||||
largest_uid_skipped = Some(uid);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
largest_uid_skipped = Some(uid);
|
||||
}
|
||||
@@ -747,29 +759,10 @@ impl Imap {
|
||||
};
|
||||
|
||||
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,
|
||||
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(())
|
||||
session
|
||||
.fetch_many_msgs(context, folder, uids_fetch, &uid_message_ids, sender)
|
||||
.await
|
||||
.context("fetch_many_msgs")
|
||||
};
|
||||
|
||||
let (largest_uid_fetched, fetch_res) =
|
||||
@@ -804,33 +797,36 @@ impl Imap {
|
||||
|
||||
chat::mark_old_messages_as_noticed(context, received_msgs).await?;
|
||||
|
||||
if fetch_res.is_ok() {
|
||||
info!(
|
||||
context,
|
||||
"available_post_msgs: {}, download_later: {}.",
|
||||
available_post_msgs.len(),
|
||||
download_later.len(),
|
||||
);
|
||||
let trans_fn = |t: &mut rusqlite::Transaction| {
|
||||
let mut stmt = t.prepare("INSERT OR IGNORE INTO available_post_msgs VALUES (?)")?;
|
||||
for rfc724_mid in available_post_msgs {
|
||||
stmt.execute((rfc724_mid,))
|
||||
.context("INSERT OR IGNORE INTO available_post_msgs")?;
|
||||
}
|
||||
let mut stmt =
|
||||
t.prepare("INSERT OR IGNORE INTO download (rfc724_mid, msg_id) VALUES (?,0)")?;
|
||||
for rfc724_mid in download_later {
|
||||
stmt.execute((rfc724_mid,))
|
||||
.context("INSERT OR IGNORE INTO download")?;
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
context.sql.transaction(trans_fn).await?;
|
||||
}
|
||||
|
||||
// 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.
|
||||
/// This way, we can already offer them some email addresses they can write to.
|
||||
///
|
||||
/// Then, Fetch the last messages DC_FETCH_EXISTING_MSGS_COUNT emails from the server
|
||||
/// and show them in the chat list.
|
||||
pub(crate) async fn fetch_existing_msgs(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
session: &mut Session,
|
||||
) -> Result<()> {
|
||||
add_all_recipients_as_contacts(context, session, Config::ConfiguredMvboxFolder)
|
||||
.await
|
||||
.context("failed to get recipients from the movebox")?;
|
||||
add_all_recipients_as_contacts(context, session, Config::ConfiguredInboxFolder)
|
||||
.await
|
||||
.context("failed to get recipients from the inbox")?;
|
||||
|
||||
info!(context, "Done fetching existing messages.");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Session {
|
||||
@@ -869,10 +865,7 @@ impl Session {
|
||||
// Collect pairs of UID and Message-ID.
|
||||
let mut msgs = BTreeMap::new();
|
||||
|
||||
let create = false;
|
||||
let folder_exists = self
|
||||
.select_with_uidvalidity(context, folder, create)
|
||||
.await?;
|
||||
let folder_exists = self.select_with_uidvalidity(context, folder).await?;
|
||||
let transport_id = self.transport_id();
|
||||
if folder_exists {
|
||||
let mut list = self
|
||||
@@ -916,7 +909,7 @@ impl Session {
|
||||
context
|
||||
.sql
|
||||
.transaction(move |transaction| {
|
||||
transaction.execute("DELETE FROM imap WHERE folder=?", (folder,))?;
|
||||
transaction.execute("DELETE FROM imap WHERE transport_id=? AND folder=?", (transport_id, folder,))?;
|
||||
for (uid, (rfc724_mid, target)) in &msgs {
|
||||
// This may detect previously undetected moved
|
||||
// messages, so we update server_folder too.
|
||||
@@ -994,17 +987,6 @@ impl Session {
|
||||
return Ok(());
|
||||
}
|
||||
Err(err) => {
|
||||
if context.should_delete_to_trash().await? {
|
||||
error!(
|
||||
context,
|
||||
"Cannot move messages {} to {}, no fallback to COPY/DELETE because \
|
||||
delete_to_trash is set. Error: {:#}",
|
||||
set,
|
||||
target,
|
||||
err,
|
||||
);
|
||||
return Err(err.into());
|
||||
}
|
||||
warn!(
|
||||
context,
|
||||
"Cannot move messages, fallback to COPY/DELETE {} to {}: {}",
|
||||
@@ -1018,19 +1000,11 @@ impl Session {
|
||||
|
||||
// Server does not support MOVE or MOVE failed.
|
||||
// Copy messages to the destination folder if needed and mark records for deletion.
|
||||
let copy = !context.is_trash(target).await?;
|
||||
if copy {
|
||||
info!(
|
||||
context,
|
||||
"Server does not support MOVE, fallback to COPY/DELETE {} to {}", set, target
|
||||
);
|
||||
self.uid_copy(&set, &target).await?;
|
||||
} else {
|
||||
error!(
|
||||
context,
|
||||
"Server does not support MOVE, fallback to DELETE {} to {}", set, target,
|
||||
);
|
||||
}
|
||||
info!(
|
||||
context,
|
||||
"Server does not support MOVE, fallback to COPY/DELETE {} to {}", set, target
|
||||
);
|
||||
self.uid_copy(&set, &target).await?;
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
@@ -1042,11 +1016,9 @@ impl Session {
|
||||
})
|
||||
.await
|
||||
.context("Cannot plan deletion of messages")?;
|
||||
if copy {
|
||||
context.emit_event(EventType::ImapMessageMoved(format!(
|
||||
"IMAP messages {set} copied to {target}"
|
||||
)));
|
||||
}
|
||||
context.emit_event(EventType::ImapMessageMoved(format!(
|
||||
"IMAP messages {set} copied to {target}"
|
||||
)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1054,14 +1026,16 @@ impl Session {
|
||||
///
|
||||
/// This is the only place where messages are moved or deleted on the IMAP server.
|
||||
async fn move_delete_messages(&mut self, context: &Context, folder: &str) -> Result<()> {
|
||||
let transport_id = self.transport_id();
|
||||
let rows = context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT id, uid, target FROM imap
|
||||
WHERE folder = ?
|
||||
AND target != folder
|
||||
ORDER BY target, uid",
|
||||
(folder,),
|
||||
WHERE folder = ?
|
||||
AND transport_id = ?
|
||||
AND target != folder
|
||||
ORDER BY target, uid",
|
||||
(folder, transport_id),
|
||||
|row| {
|
||||
let rowid: i64 = row.get(0)?;
|
||||
let uid: u32 = row.get(1)?;
|
||||
@@ -1076,10 +1050,7 @@ impl Session {
|
||||
// MOVE/DELETE operations. This does not result in multiple SELECT commands
|
||||
// being sent because `select_folder()` does nothing if the folder is already
|
||||
// selected.
|
||||
let create = false;
|
||||
let folder_exists = self
|
||||
.select_with_uidvalidity(context, folder, create)
|
||||
.await?;
|
||||
let folder_exists = self.select_with_uidvalidity(context, folder).await?;
|
||||
ensure!(folder_exists, "No folder {folder}");
|
||||
|
||||
// Empty target folder name means messages should be deleted.
|
||||
@@ -1108,61 +1079,22 @@ impl Session {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Uploads sync messages from the `imap_send` table with `\Seen` flag set.
|
||||
pub(crate) async fn send_sync_msgs(&mut self, context: &Context, folder: &str) -> Result<()> {
|
||||
context.send_sync_msg().await?;
|
||||
while let Some((id, mime, msg_id, attempts)) = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT id, mime, msg_id, attempts FROM imap_send ORDER BY id LIMIT 1",
|
||||
(),
|
||||
|row| {
|
||||
let id: i64 = row.get(0)?;
|
||||
let mime: String = row.get(1)?;
|
||||
let msg_id: MsgId = row.get(2)?;
|
||||
let attempts: i64 = row.get(3)?;
|
||||
Ok((id, mime, msg_id, attempts))
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("Failed to SELECT from imap_send")?
|
||||
{
|
||||
let res = self
|
||||
.append(folder, Some("(\\Seen)"), None, mime)
|
||||
.await
|
||||
.with_context(|| format!("IMAP APPEND to {folder} failed for {msg_id}"))
|
||||
.log_err(context);
|
||||
if res.is_ok() {
|
||||
msg_id.set_delivered(context).await?;
|
||||
}
|
||||
const MAX_ATTEMPTS: i64 = 2;
|
||||
if res.is_ok() || attempts >= MAX_ATTEMPTS - 1 {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM imap_send WHERE id=?", (id,))
|
||||
.await
|
||||
.context("Failed to delete from imap_send")?;
|
||||
} else {
|
||||
context
|
||||
.sql
|
||||
.execute("UPDATE imap_send SET attempts=attempts+1 WHERE id=?", (id,))
|
||||
.await
|
||||
.context("Failed to update imap_send.attempts")?;
|
||||
res?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stores pending `\Seen` flags for messages in `imap_markseen` table.
|
||||
pub(crate) async fn store_seen_flags_on_imap(&mut self, context: &Context) -> Result<()> {
|
||||
if context.get_config_bool(Config::TeamProfile).await? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let transport_id = self.transport_id();
|
||||
let rows = context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT imap.id, uid, folder FROM imap, imap_markseen
|
||||
WHERE imap.id = imap_markseen.id AND target = folder
|
||||
WHERE imap.id = imap_markseen.id
|
||||
AND imap.transport_id=?
|
||||
AND target = folder
|
||||
ORDER BY folder, uid",
|
||||
[],
|
||||
(transport_id,),
|
||||
|row| {
|
||||
let rowid: i64 = row.get(0)?;
|
||||
let uid: u32 = row.get(1)?;
|
||||
@@ -1173,8 +1105,7 @@ impl Session {
|
||||
.await?;
|
||||
|
||||
for (folder, rowid_set, uid_set) in UidGrouper::from(rows) {
|
||||
let create = false;
|
||||
let folder_exists = match self.select_with_uidvalidity(context, &folder, create).await {
|
||||
let folder_exists = match self.select_with_uidvalidity(context, &folder).await {
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
@@ -1224,9 +1155,12 @@ impl Session {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let create = false;
|
||||
if context.get_config_bool(Config::TeamProfile).await? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let folder_exists = self
|
||||
.select_with_uidvalidity(context, folder, create)
|
||||
.select_with_uidvalidity(context, folder)
|
||||
.await
|
||||
.context("Failed to select folder")?;
|
||||
if !folder_exists {
|
||||
@@ -1277,10 +1211,10 @@ impl Session {
|
||||
};
|
||||
let is_seen = fetch.flags().any(|flag| flag == Flag::Seen);
|
||||
if is_seen
|
||||
&& let Some(chat_id) = mark_seen_by_uid(context, folder, uid_validity, uid)
|
||||
&& let Some(chat_id) = mark_seen_by_uid(context, transport_id, folder, uid_validity, uid)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("failed to update seen status for msg {folder}/{uid}")
|
||||
format!("Transport {transport_id}: Failed to update seen status for msg {folder}/{uid}")
|
||||
})?
|
||||
{
|
||||
updated_chat_ids.insert(chat_id);
|
||||
@@ -1318,41 +1252,6 @@ impl Session {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the from, to and bcc addresses from all existing outgoing emails.
|
||||
pub async fn get_all_recipients(&mut self, context: &Context) -> Result<Vec<SingleInfo>> {
|
||||
let mut uids: Vec<_> = self
|
||||
.uid_search(get_imap_self_sent_search_command(context).await?)
|
||||
.await?
|
||||
.into_iter()
|
||||
.collect();
|
||||
uids.sort_unstable();
|
||||
|
||||
let mut result = Vec::new();
|
||||
for (_, uid_set) in build_sequence_sets(&uids)? {
|
||||
let mut list = self
|
||||
.uid_fetch(uid_set, "(UID BODY.PEEK[HEADER.FIELDS (FROM TO CC BCC)])")
|
||||
.await
|
||||
.context("IMAP Could not fetch")?;
|
||||
|
||||
while let Some(msg) = list.try_next().await? {
|
||||
match get_fetch_headers(&msg) {
|
||||
Ok(headers) => {
|
||||
if let Some(from) = mimeparser::get_from(&headers)
|
||||
&& context.is_self_addr(&from.addr).await?
|
||||
{
|
||||
result.extend(mimeparser::get_recipients(&headers));
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "{}", err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Fetches a list of messages by server UID.
|
||||
///
|
||||
/// Sends pairs of UID and info about each downloaded message to the provided channel.
|
||||
@@ -1373,7 +1272,6 @@ impl Session {
|
||||
folder: &str,
|
||||
request_uids: Vec<u32>,
|
||||
uid_message_ids: &BTreeMap<u32, String>,
|
||||
fetch_partially: bool,
|
||||
received_msgs_channel: Sender<(u32, Option<ReceivedMsg>)>,
|
||||
) -> Result<()> {
|
||||
if request_uids.is_empty() {
|
||||
@@ -1381,25 +1279,10 @@ impl Session {
|
||||
}
|
||||
|
||||
for (request_uids, set) in build_sequence_sets(&request_uids)? {
|
||||
info!(
|
||||
context,
|
||||
"Starting a {} FETCH of message set \"{}\".",
|
||||
if fetch_partially { "partial" } else { "full" },
|
||||
set
|
||||
);
|
||||
let mut fetch_responses = self
|
||||
.uid_fetch(
|
||||
&set,
|
||||
if fetch_partially {
|
||||
BODY_PARTIAL
|
||||
} else {
|
||||
BODY_FULL
|
||||
},
|
||||
)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("fetching messages {} from folder \"{}\"", &set, folder)
|
||||
})?;
|
||||
info!(context, "Starting UID FETCH of message set \"{}\".", set);
|
||||
let mut fetch_responses = self.uid_fetch(&set, BODY_FULL).await.with_context(|| {
|
||||
format!("fetching messages {} from folder \"{}\"", &set, folder)
|
||||
})?;
|
||||
|
||||
// Map from UIDs to unprocessed FETCH results. We put unprocessed FETCH results here
|
||||
// when we want to process other messages first.
|
||||
@@ -1456,11 +1339,7 @@ impl Session {
|
||||
count += 1;
|
||||
|
||||
let is_deleted = fetch_response.flags().any(|flag| flag == Flag::Deleted);
|
||||
let (body, partial) = if fetch_partially {
|
||||
(fetch_response.header(), fetch_response.size) // `BODY.PEEK[HEADER]` goes to header() ...
|
||||
} else {
|
||||
(fetch_response.body(), None) // ... while `BODY.PEEK[]` goes to body() - and includes header()
|
||||
};
|
||||
let body = fetch_response.body();
|
||||
|
||||
if is_deleted {
|
||||
info!(context, "Not processing deleted msg {}.", request_uid);
|
||||
@@ -1494,13 +1373,13 @@ impl Session {
|
||||
context,
|
||||
"Passing message UID {} to receive_imf().", request_uid
|
||||
);
|
||||
let res = receive_imf_inner(context, rfc724_mid, body, is_seen, partial).await;
|
||||
let res = receive_imf_inner(context, rfc724_mid, body, is_seen).await;
|
||||
let received_msg = match res {
|
||||
Err(err) => {
|
||||
warn!(context, "receive_imf error: {err:#}.");
|
||||
|
||||
let text = format!(
|
||||
"❌ Failed to receive a message: {err:#}. Please report this bug to delta@merlinux.eu or https://support.delta.chat/."
|
||||
"❌ Failed to receive a message: {err:#}. Core version v{DC_VERSION_STR}. Please report this bug to delta@merlinux.eu or https://support.delta.chat/.",
|
||||
);
|
||||
let mut msg = Message::new_text(text);
|
||||
add_device_msg(context, None, Some(&mut msg)).await?;
|
||||
@@ -1546,17 +1425,17 @@ impl Session {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieves server metadata if it is supported.
|
||||
/// Retrieves server metadata if it is supported, otherwise uses fallback one.
|
||||
///
|
||||
/// We get [`/shared/comment`](https://www.rfc-editor.org/rfc/rfc5464#section-6.2.1)
|
||||
/// and [`/shared/admin`](https://www.rfc-editor.org/rfc/rfc5464#section-6.2.2)
|
||||
/// metadata.
|
||||
pub(crate) async fn fetch_metadata(&mut self, context: &Context) -> Result<()> {
|
||||
if !self.can_metadata() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
pub(crate) async fn update_metadata(&mut self, context: &Context) -> Result<()> {
|
||||
let mut lock = context.metadata.write().await;
|
||||
|
||||
if !self.can_metadata() {
|
||||
*lock = Some(Default::default());
|
||||
}
|
||||
if let Some(ref mut old_metadata) = *lock {
|
||||
let now = time();
|
||||
|
||||
@@ -1565,34 +1444,36 @@ impl Session {
|
||||
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"
|
||||
&& 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 self.can_metadata() {
|
||||
info!(context, "ICE servers expired, requesting new credentials.");
|
||||
let mailbox = "";
|
||||
let options = "";
|
||||
let metadata = self
|
||||
.get_metadata(mailbox, options, "(/shared/vendor/deltachat/turn)")
|
||||
.await?;
|
||||
for m in metadata {
|
||||
if m.entry == "/shared/vendor/deltachat/turn"
|
||||
&& let Some(value) = m.value
|
||||
{
|
||||
match create_ice_servers_from_metadata(&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 = true;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Failed to parse TURN server metadata: {err:#}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !got_turn_server {
|
||||
info!(context, "Will use fallback ICE servers.");
|
||||
// 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?;
|
||||
old_metadata.ice_servers = create_fallback_ice_servers();
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
@@ -1639,7 +1520,7 @@ impl Session {
|
||||
}
|
||||
"/shared/vendor/deltachat/turn" => {
|
||||
if let Some(value) = m.value {
|
||||
match create_ice_servers_from_metadata(context, &value).await {
|
||||
match create_ice_servers_from_metadata(&value).await {
|
||||
Ok((parsed_timestamp, parsed_ice_servers)) => {
|
||||
ice_servers_expiration_timestamp = parsed_timestamp;
|
||||
ice_servers = Some(parsed_ice_servers);
|
||||
@@ -1658,7 +1539,7 @@ impl Session {
|
||||
} 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?
|
||||
create_fallback_ice_servers()
|
||||
};
|
||||
|
||||
*lock = Some(ServerMetadata {
|
||||
@@ -1790,17 +1671,16 @@ impl Session {
|
||||
|
||||
/// Attempts to configure mvbox.
|
||||
///
|
||||
/// Tries to find any folder examining `folders` in the order they go. If none is found, tries
|
||||
/// to create any folder in the same order. This method does not use LIST command to ensure that
|
||||
/// Tries to find any folder examining `folders` in the order they go.
|
||||
/// This method does not use LIST command to ensure that
|
||||
/// configuration works even if mailbox lookup is forbidden via Access Control List (see
|
||||
/// <https://datatracker.ietf.org/doc/html/rfc4314>).
|
||||
///
|
||||
/// Returns first found or created folder name.
|
||||
/// Returns first found folder name.
|
||||
async fn configure_mvbox<'a>(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
folders: &[&'a str],
|
||||
create_mvbox: bool,
|
||||
) -> Result<Option<&'a str>> {
|
||||
// Close currently selected folder if needed.
|
||||
// We are going to select folders using low-level EXAMINE operations below.
|
||||
@@ -1817,34 +1697,12 @@ impl Session {
|
||||
self.close().await?;
|
||||
// Before moving emails to the mvbox we need to remember its UIDVALIDITY, otherwise
|
||||
// emails moved before that wouldn't be fetched but considered "old" instead.
|
||||
let create = false;
|
||||
let folder_exists = self
|
||||
.select_with_uidvalidity(context, folder, create)
|
||||
.await?;
|
||||
let folder_exists = self.select_with_uidvalidity(context, folder).await?;
|
||||
ensure!(folder_exists, "No MVBOX folder {:?}??", &folder);
|
||||
return Ok(Some(folder));
|
||||
}
|
||||
}
|
||||
|
||||
if !create_mvbox {
|
||||
return Ok(None);
|
||||
}
|
||||
// Some servers require namespace-style folder names like "INBOX.DeltaChat", so we try all
|
||||
// the variants here.
|
||||
for folder in folders {
|
||||
match self
|
||||
.select_with_uidvalidity(context, folder, create_mvbox)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
info!(context, "MVBOX-folder {} created.", folder);
|
||||
return Ok(Some(folder));
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Cannot create MVBOX-folder {:?}: {}", folder, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
@@ -1854,7 +1712,6 @@ impl Imap {
|
||||
&mut self,
|
||||
context: &Context,
|
||||
session: &mut Session,
|
||||
create_mvbox: bool,
|
||||
) -> Result<()> {
|
||||
let mut folders = session
|
||||
.list(Some(""), Some("*"))
|
||||
@@ -1895,7 +1752,7 @@ impl Imap {
|
||||
|
||||
let fallback_folder = format!("INBOX{delimiter}DeltaChat");
|
||||
let mvbox_folder = session
|
||||
.configure_mvbox(context, &["DeltaChat", &fallback_folder], create_mvbox)
|
||||
.configure_mvbox(context, &["DeltaChat", &fallback_folder])
|
||||
.await
|
||||
.context("failed to configure mvbox")?;
|
||||
|
||||
@@ -2109,17 +1966,6 @@ async fn needs_move_to_mvbox(
|
||||
headers: &[mailparse::MailHeader<'_>],
|
||||
) -> Result<bool> {
|
||||
let has_chat_version = headers.get_header_value(HeaderDef::ChatVersion).is_some();
|
||||
if !context.get_config_bool(Config::IsChatmail).await?
|
||||
&& has_chat_version
|
||||
&& headers
|
||||
.get_header_value(HeaderDef::AutoSubmitted)
|
||||
.filter(|val| val.eq_ignore_ascii_case("auto-generated"))
|
||||
.is_some()
|
||||
&& let Some(from) = mimeparser::get_from(headers)
|
||||
&& context.is_self_addr(&from.addr).await?
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
if !context.get_config_bool(Config::MvboxMove).await? {
|
||||
return Ok(false);
|
||||
}
|
||||
@@ -2255,21 +2101,6 @@ pub(crate) fn create_message_id() -> String {
|
||||
format!("{}{}", GENERATED_PREFIX, create_id())
|
||||
}
|
||||
|
||||
/// Returns chat by prefetched headers.
|
||||
async fn prefetch_get_chat(
|
||||
context: &Context,
|
||||
headers: &[mailparse::MailHeader<'_>],
|
||||
) -> Result<Option<chat::Chat>> {
|
||||
let parent = get_prefetch_parent_message(context, headers).await?;
|
||||
if let Some(parent) = &parent {
|
||||
return Ok(Some(
|
||||
chat::Chat::load_from_db(context, parent.get_chat_id()).await?,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Determines whether the message should be downloaded based on prefetched headers.
|
||||
pub(crate) async fn prefetch_should_download(
|
||||
context: &Context,
|
||||
@@ -2277,26 +2108,18 @@ pub(crate) async fn prefetch_should_download(
|
||||
message_id: &str,
|
||||
mut flags: impl Iterator<Item = Flag<'_>>,
|
||||
) -> Result<bool> {
|
||||
if message::rfc724_mid_exists(context, message_id)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
markseen_on_imap_table(context, message_id).await?;
|
||||
if message::rfc724_mid_download_tried(context, message_id).await? {
|
||||
if let Some(from) = mimeparser::get_from(headers)
|
||||
&& context.is_self_addr(&from.addr).await?
|
||||
{
|
||||
markseen_on_imap_table(context, message_id).await?;
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// We do not know the Message-ID or the Message-ID is missing (in this case, we create one in
|
||||
// the further process).
|
||||
|
||||
if let Some(chat) = prefetch_get_chat(context, headers).await?
|
||||
&& chat.typ == Chattype::Group
|
||||
&& !chat.id.is_special()
|
||||
{
|
||||
// This might be a group command, like removing a group member.
|
||||
// We really need to fetch this to avoid inconsistent group state.
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let maybe_ndn = if let Some(from) = headers.get_header_value(HeaderDef::From_) {
|
||||
let from = from.to_ascii_lowercase();
|
||||
from.contains("mailer-daemon") || from.contains("mail-daemon")
|
||||
@@ -2357,6 +2180,7 @@ pub(crate) async fn prefetch_should_download(
|
||||
/// Returns updated chat ID if any message was marked as seen.
|
||||
async fn mark_seen_by_uid(
|
||||
context: &Context,
|
||||
transport_id: u32,
|
||||
folder: &str,
|
||||
uid_validity: u32,
|
||||
uid: u32,
|
||||
@@ -2367,12 +2191,13 @@ async fn mark_seen_by_uid(
|
||||
"SELECT id, chat_id FROM msgs
|
||||
WHERE id > 9 AND rfc724_mid IN (
|
||||
SELECT rfc724_mid FROM imap
|
||||
WHERE folder=?1
|
||||
AND uidvalidity=?2
|
||||
AND uid=?3
|
||||
WHERE transport_id=?
|
||||
AND folder=?
|
||||
AND uidvalidity=?
|
||||
AND uid=?
|
||||
LIMIT 1
|
||||
)",
|
||||
(&folder, uid_validity, uid),
|
||||
(transport_id, &folder, uid_validity, uid),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
let chat_id: ChatId = row.get(1)?;
|
||||
@@ -2523,18 +2348,6 @@ async fn get_modseq(context: &Context, transport_id: u32, folder: &str) -> Resul
|
||||
.unwrap_or(0))
|
||||
}
|
||||
|
||||
/// Compute the imap search expression for all self-sent mails (for all self addresses)
|
||||
pub(crate) async fn get_imap_self_sent_search_command(context: &Context) -> Result<String> {
|
||||
// See https://www.rfc-editor.org/rfc/rfc3501#section-6.4.4 for syntax of SEARCH and OR
|
||||
let mut search_command = format!("FROM \"{}\"", context.get_primary_self_addr().await?);
|
||||
|
||||
for item in context.get_secondary_self_addrs().await? {
|
||||
search_command = format!("OR ({search_command}) (FROM \"{item}\")");
|
||||
}
|
||||
|
||||
Ok(search_command)
|
||||
}
|
||||
|
||||
/// Whether to ignore fetching messages from a folder.
|
||||
///
|
||||
/// This caters for the [`Config::OnlyFetchMvbox`] setting which means mails from folders
|
||||
@@ -2607,66 +2420,6 @@ impl std::fmt::Display for UidRange {
|
||||
}
|
||||
}
|
||||
}
|
||||
async fn add_all_recipients_as_contacts(
|
||||
context: &Context,
|
||||
session: &mut Session,
|
||||
folder: Config,
|
||||
) -> Result<()> {
|
||||
let mailbox = if let Some(m) = context.get_config(folder).await? {
|
||||
m
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"Folder {} is not configured, skipping fetching contacts from it.", folder
|
||||
);
|
||||
return Ok(());
|
||||
};
|
||||
let create = false;
|
||||
let folder_exists = session
|
||||
.select_with_uidvalidity(context, &mailbox, create)
|
||||
.await
|
||||
.with_context(|| format!("could not select {mailbox}"))?;
|
||||
if !folder_exists {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let recipients = session
|
||||
.get_all_recipients(context)
|
||||
.await
|
||||
.context("could not get recipients")?;
|
||||
|
||||
let mut any_modified = false;
|
||||
for recipient in recipients {
|
||||
let recipient_addr = match ContactAddress::new(&recipient.addr) {
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Could not add contact for recipient with address {:?}: {:#}",
|
||||
recipient.addr,
|
||||
err
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Ok(recipient_addr) => recipient_addr,
|
||||
};
|
||||
|
||||
let (_, modified) = Contact::add_or_lookup(
|
||||
context,
|
||||
&recipient.display_name.unwrap_or_default(),
|
||||
&recipient_addr,
|
||||
Origin::OutgoingTo,
|
||||
)
|
||||
.await?;
|
||||
if modified != Modifier::None {
|
||||
any_modified = true;
|
||||
}
|
||||
}
|
||||
if any_modified {
|
||||
context.emit_event(EventType::ContactsChanged(None));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod imap_tests;
|
||||
|
||||
@@ -27,9 +27,7 @@ impl Session {
|
||||
idle_interrupt_receiver: Receiver<()>,
|
||||
folder: &str,
|
||||
) -> Result<Self> {
|
||||
let create = true;
|
||||
self.select_with_uidvalidity(context, folder, create)
|
||||
.await?;
|
||||
self.select_with_uidvalidity(context, folder).await?;
|
||||
|
||||
if self.drain_unsolicited_responses(context)? {
|
||||
self.new_mail = true;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::*;
|
||||
use crate::contact::Contact;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::transport::add_pseudo_transport;
|
||||
|
||||
#[test]
|
||||
fn test_get_folder_meaning_by_name() {
|
||||
@@ -264,31 +264,6 @@ async fn test_target_folder_setupmsg() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_imap_search_command() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
assert_eq!(
|
||||
get_imap_self_sent_search_command(&t.ctx).await?,
|
||||
r#"FROM "alice@example.org""#
|
||||
);
|
||||
|
||||
add_pseudo_transport(&t, "alice@another.com").await?;
|
||||
t.ctx.set_primary_self_addr("alice@another.com").await?;
|
||||
assert_eq!(
|
||||
get_imap_self_sent_search_command(&t.ctx).await?,
|
||||
r#"OR (FROM "alice@another.com") (FROM "alice@example.org")"#
|
||||
);
|
||||
|
||||
add_pseudo_transport(&t, "alice@third.com").await?;
|
||||
t.ctx.set_primary_self_addr("alice@third.com").await?;
|
||||
assert_eq!(
|
||||
get_imap_self_sent_search_command(&t.ctx).await?,
|
||||
r#"OR (OR (FROM "alice@third.com") (FROM "alice@another.com")) (FROM "alice@example.org")"#
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_uid_grouper() {
|
||||
// Input: sequence of (rowid: i64, uid: u32, target: String)
|
||||
|
||||
@@ -84,17 +84,6 @@ impl Imap {
|
||||
}
|
||||
}
|
||||
|
||||
// Set config for the Trash folder. Or reset if the folder was deleted.
|
||||
let conf = Config::ConfiguredTrashFolder;
|
||||
let val = folder_configs.get(&conf).map(|s| s.as_str());
|
||||
let interrupt = val.is_some() && context.get_config(conf).await?.is_none();
|
||||
context.set_config_internal(conf, val).await?;
|
||||
if interrupt {
|
||||
// `Imap::fetch_move_delete()`, particularly message deletion, is possible now for other
|
||||
// folders (NB: we are in the Inbox loop).
|
||||
context.scheduler.interrupt_oboxes().await;
|
||||
}
|
||||
|
||||
info!(context, "Found folders: {folder_names:?}.");
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
@@ -89,33 +89,6 @@ impl ImapSession {
|
||||
}
|
||||
}
|
||||
|
||||
/// Selects a folder. Tries to create it once and select again if the folder does not exist.
|
||||
pub(super) async fn select_or_create_folder(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
folder: &str,
|
||||
) -> anyhow::Result<NewlySelected> {
|
||||
match self.select_folder(context, folder).await {
|
||||
Ok(newly_selected) => Ok(newly_selected),
|
||||
Err(err) => match err {
|
||||
Error::NoFolder(..) => {
|
||||
info!(context, "Failed to select folder {folder:?} because it does not exist, trying to create it.");
|
||||
let create_res = self.create(folder).await;
|
||||
if let Err(ref err) = create_res {
|
||||
info!(context, "Couldn't select folder, then create() failed: {err:#}.");
|
||||
// Need to recheck, could have been created in parallel.
|
||||
}
|
||||
let select_res = self.select_folder(context, folder).await.with_context(|| format!("failed to select newely created folder {folder}"));
|
||||
if select_res.is_err() {
|
||||
create_res?;
|
||||
}
|
||||
select_res
|
||||
}
|
||||
_ => Err(err).with_context(|| format!("failed to select folder {folder} with error other than NO, not trying to create it")),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Selects a folder optionally creating it and takes care of UIDVALIDITY changes. Returns false
|
||||
/// iff `folder` doesn't exist.
|
||||
///
|
||||
@@ -129,23 +102,16 @@ impl ImapSession {
|
||||
&mut self,
|
||||
context: &Context,
|
||||
folder: &str,
|
||||
create: bool,
|
||||
) -> anyhow::Result<bool> {
|
||||
let newly_selected = if create {
|
||||
self.select_or_create_folder(context, folder)
|
||||
.await
|
||||
.with_context(|| format!("Failed to select or create folder {folder:?}"))?
|
||||
} else {
|
||||
match self.select_folder(context, folder).await {
|
||||
Ok(newly_selected) => newly_selected,
|
||||
Err(err) => match err {
|
||||
Error::NoFolder(..) => return Ok(false),
|
||||
_ => {
|
||||
return Err(err)
|
||||
.with_context(|| format!("Failed to select folder {folder:?}"))?;
|
||||
}
|
||||
},
|
||||
}
|
||||
let newly_selected = match self.select_folder(context, folder).await {
|
||||
Ok(newly_selected) => newly_selected,
|
||||
Err(err) => match err {
|
||||
Error::NoFolder(..) => return Ok(false),
|
||||
_ => {
|
||||
return Err(err)
|
||||
.with_context(|| format!("Failed to select folder {folder:?}"))?;
|
||||
}
|
||||
},
|
||||
};
|
||||
let transport_id = self.transport_id();
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ use crate::tools;
|
||||
/// - Chat-Version to check if a message is a chat message
|
||||
/// - Autocrypt-Setup-Message to check if a message is an autocrypt setup message,
|
||||
/// not necessarily sent by Delta Chat.
|
||||
/// - Chat-Is-Post-Message to skip it in background fetch or when it is > `DownloadLimit`.
|
||||
const PREFETCH_FLAGS: &str = "(UID INTERNALDATE RFC822.SIZE BODY.PEEK[HEADER.FIELDS (\
|
||||
MESSAGE-ID \
|
||||
DATE \
|
||||
@@ -24,6 +25,7 @@ const PREFETCH_FLAGS: &str = "(UID INTERNALDATE RFC822.SIZE BODY.PEEK[HEADER.FIE
|
||||
FROM \
|
||||
IN-REPLY-TO REFERENCES \
|
||||
CHAT-VERSION \
|
||||
CHAT-IS-POST-MESSAGE \
|
||||
AUTO-SUBMITTED \
|
||||
AUTOCRYPT-SETUP-MESSAGE\
|
||||
)])";
|
||||
|
||||
@@ -21,7 +21,7 @@ pub async fn store_self_keypair(context: &Context, keypair: &KeyPair) -> Result<
|
||||
}
|
||||
|
||||
pub async fn parse_and_get_text(context: &Context, imf_raw: &[u8]) -> Result<String> {
|
||||
let mime_parser = MimeMessage::from_bytes(context, imf_raw, None).await?;
|
||||
let mime_parser = MimeMessage::from_bytes(context, imf_raw).await?;
|
||||
Ok(mime_parser.parts.into_iter().next().unwrap().msg)
|
||||
}
|
||||
|
||||
|
||||
24
src/key.rs
24
src/key.rs
@@ -156,24 +156,14 @@ pub(crate) async fn load_self_public_key(context: &Context) -> Result<SignedPubl
|
||||
}
|
||||
|
||||
/// Returns our own public keyring.
|
||||
///
|
||||
/// No keys are generated and at most one key is returned.
|
||||
pub(crate) async fn load_self_public_keyring(context: &Context) -> Result<Vec<SignedPublicKey>> {
|
||||
let keys = context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
r#"SELECT public_key
|
||||
FROM keypairs
|
||||
ORDER BY id=(SELECT value FROM config WHERE keyname='key_id') DESC"#,
|
||||
(),
|
||||
|row| {
|
||||
let public_key_bytes: Vec<u8> = row.get(0)?;
|
||||
Ok(public_key_bytes)
|
||||
},
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter_map(|bytes| SignedPublicKey::from_slice(&bytes).log_err(context).ok())
|
||||
.collect();
|
||||
Ok(keys)
|
||||
if let Some(public_key) = load_self_public_key_opt(context).await? {
|
||||
Ok(vec![public_key])
|
||||
} else {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns own public key fingerprint in (not human-readable) hex representation.
|
||||
|
||||
297
src/message.rs
297
src/message.rs
@@ -8,6 +8,9 @@ use std::str;
|
||||
use anyhow::{Context as _, Result, ensure, format_err};
|
||||
use deltachat_contact_tools::{VcardContact, parse_vcard};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use humansize::BINARY;
|
||||
use humansize::format_size;
|
||||
use num_traits::FromPrimitive;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::{fs, io};
|
||||
|
||||
@@ -84,12 +87,10 @@ impl MsgId {
|
||||
let result = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
concat!(
|
||||
"SELECT m.state, mdns.msg_id",
|
||||
" FROM msgs m LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id",
|
||||
" WHERE id=?",
|
||||
" LIMIT 1",
|
||||
),
|
||||
"SELECT m.state, mdns.msg_id
|
||||
FROM msgs m LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id
|
||||
WHERE id=?
|
||||
LIMIT 1",
|
||||
(self,),
|
||||
|row| {
|
||||
let state: MessageState = row.get(0)?;
|
||||
@@ -128,10 +129,12 @@ impl MsgId {
|
||||
.sql
|
||||
.execute(
|
||||
// If you change which information is preserved here, also change
|
||||
// `delete_expired_messages()` and which information `receive_imf::add_parts()`
|
||||
// still adds to the db if chat_id is TRASH.
|
||||
"INSERT OR REPLACE INTO msgs (id, rfc724_mid, timestamp, chat_id, deleted)
|
||||
SELECT ?1, rfc724_mid, timestamp, ?, ? FROM msgs WHERE id=?1",
|
||||
// `ChatId::delete_ex()`, `delete_expired_messages()` and which information
|
||||
// `receive_imf::add_parts()` still adds to the db if chat_id is TRASH.
|
||||
"
|
||||
INSERT OR REPLACE INTO msgs (id, rfc724_mid, pre_rfc724_mid, timestamp, chat_id, deleted)
|
||||
SELECT ?1, rfc724_mid, pre_rfc724_mid, timestamp, ?, ? FROM msgs WHERE id=?1
|
||||
",
|
||||
(self, DC_CHAT_ID_TRASH, on_server),
|
||||
)
|
||||
.await?;
|
||||
@@ -171,12 +174,17 @@ impl MsgId {
|
||||
context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT folder, uid FROM imap WHERE rfc724_mid=?",
|
||||
"SELECT transports.addr, imap.folder, imap.uid
|
||||
FROM imap
|
||||
LEFT JOIN transports
|
||||
ON transports.id = imap.transport_id
|
||||
WHERE imap.rfc724_mid=?",
|
||||
(rfc724_mid,),
|
||||
|row| {
|
||||
let folder: String = row.get("folder")?;
|
||||
let uid: u32 = row.get("uid")?;
|
||||
Ok(format!("</{folder}/;UID={uid}>"))
|
||||
let addr: String = row.get(0)?;
|
||||
let folder: String = row.get(1)?;
|
||||
let uid: u32 = row.get(2)?;
|
||||
Ok(format!("<{addr}/{folder}/;UID={uid}>"))
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -202,10 +210,9 @@ impl MsgId {
|
||||
ret += &format!("Sent: {fts}");
|
||||
|
||||
let from_contact = Contact::get_by_id(context, msg.from_id).await?;
|
||||
let name = from_contact.get_name_n_addr();
|
||||
let name = from_contact.get_display_name();
|
||||
if let Some(override_sender_name) = msg.get_override_sender_name() {
|
||||
let addr = from_contact.get_addr();
|
||||
ret += &format!(" by ~{override_sender_name} ({addr})");
|
||||
ret += &format!(" by ~{override_sender_name}");
|
||||
} else {
|
||||
ret += &format!(" by {name}");
|
||||
}
|
||||
@@ -253,7 +260,7 @@ impl MsgId {
|
||||
|
||||
let name = Contact::get_by_id(context, contact_id)
|
||||
.await
|
||||
.map(|contact| contact.get_name_n_addr())
|
||||
.map(|contact| contact.get_display_name().to_owned())
|
||||
.unwrap_or_default();
|
||||
|
||||
ret += &format!(" by {name}");
|
||||
@@ -425,6 +432,10 @@ pub struct Message {
|
||||
pub(crate) ephemeral_timer: EphemeralTimer,
|
||||
pub(crate) ephemeral_timestamp: i64,
|
||||
pub(crate) text: String,
|
||||
/// Text that is added to the end of Message.text
|
||||
///
|
||||
/// Currently used for adding the download information on pre-messages
|
||||
pub(crate) additional_text: String,
|
||||
|
||||
/// Message subject.
|
||||
///
|
||||
@@ -433,12 +444,15 @@ pub struct Message {
|
||||
|
||||
/// `Message-ID` header value.
|
||||
pub(crate) rfc724_mid: String,
|
||||
/// `Message-ID` header value of the pre-message, if any.
|
||||
pub(crate) pre_rfc724_mid: String,
|
||||
|
||||
/// `In-Reply-To` header value.
|
||||
pub(crate) in_reply_to: Option<String>,
|
||||
pub(crate) is_dc_message: MessengerMessage,
|
||||
pub(crate) original_msg_id: MsgId,
|
||||
pub(crate) mime_modified: bool,
|
||||
pub(crate) chat_visibility: ChatVisibility,
|
||||
pub(crate) chat_blocked: Blocked,
|
||||
pub(crate) location_id: u32,
|
||||
pub(crate) error: Option<String>,
|
||||
@@ -483,42 +497,42 @@ impl Message {
|
||||
!id.is_special(),
|
||||
"Can not load special message ID {id} from DB"
|
||||
);
|
||||
let msg = context
|
||||
let mut msg = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
concat!(
|
||||
"SELECT",
|
||||
" m.id AS id,",
|
||||
" rfc724_mid AS rfc724mid,",
|
||||
" m.mime_in_reply_to AS mime_in_reply_to,",
|
||||
" m.chat_id AS chat_id,",
|
||||
" m.from_id AS from_id,",
|
||||
" m.to_id AS to_id,",
|
||||
" m.timestamp AS timestamp,",
|
||||
" m.timestamp_sent AS timestamp_sent,",
|
||||
" m.timestamp_rcvd AS timestamp_rcvd,",
|
||||
" m.ephemeral_timer AS ephemeral_timer,",
|
||||
" m.ephemeral_timestamp AS ephemeral_timestamp,",
|
||||
" m.type AS type,",
|
||||
" m.state AS state,",
|
||||
" mdns.msg_id AS mdn_msg_id,",
|
||||
" m.download_state AS download_state,",
|
||||
" m.error AS error,",
|
||||
" m.msgrmsg AS msgrmsg,",
|
||||
" m.starred AS original_msg_id,",
|
||||
" m.mime_modified AS mime_modified,",
|
||||
" m.txt AS txt,",
|
||||
" m.subject AS subject,",
|
||||
" m.param AS param,",
|
||||
" m.hidden AS hidden,",
|
||||
" m.location_id AS location,",
|
||||
" c.blocked AS blocked",
|
||||
" FROM msgs m",
|
||||
" LEFT JOIN chats c ON c.id=m.chat_id",
|
||||
" LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id",
|
||||
" WHERE m.id=? AND chat_id!=3",
|
||||
" LIMIT 1",
|
||||
),
|
||||
"SELECT
|
||||
m.id AS id,
|
||||
rfc724_mid AS rfc724mid,
|
||||
pre_rfc724_mid AS pre_rfc724mid,
|
||||
m.mime_in_reply_to AS mime_in_reply_to,
|
||||
m.chat_id AS chat_id,
|
||||
m.from_id AS from_id,
|
||||
m.to_id AS to_id,
|
||||
m.timestamp AS timestamp,
|
||||
m.timestamp_sent AS timestamp_sent,
|
||||
m.timestamp_rcvd AS timestamp_rcvd,
|
||||
m.ephemeral_timer AS ephemeral_timer,
|
||||
m.ephemeral_timestamp AS ephemeral_timestamp,
|
||||
m.type AS type,
|
||||
m.state AS state,
|
||||
mdns.msg_id AS mdn_msg_id,
|
||||
m.download_state AS download_state,
|
||||
m.error AS error,
|
||||
m.msgrmsg AS msgrmsg,
|
||||
m.starred AS original_msg_id,
|
||||
m.mime_modified AS mime_modified,
|
||||
m.txt AS txt,
|
||||
m.subject AS subject,
|
||||
m.param AS param,
|
||||
m.hidden AS hidden,
|
||||
m.location_id AS location,
|
||||
c.archived AS visibility,
|
||||
c.blocked AS blocked
|
||||
FROM msgs m
|
||||
LEFT JOIN chats c ON c.id=m.chat_id
|
||||
LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id
|
||||
WHERE m.id=? AND chat_id!=3
|
||||
LIMIT 1",
|
||||
(id,),
|
||||
|row| {
|
||||
let state: MessageState = row.get("state")?;
|
||||
@@ -545,6 +559,7 @@ impl Message {
|
||||
let msg = Message {
|
||||
id: row.get("id")?,
|
||||
rfc724_mid: row.get::<_, String>("rfc724mid")?,
|
||||
pre_rfc724_mid: row.get::<_, String>("pre_rfc724mid")?,
|
||||
in_reply_to: row
|
||||
.get::<_, Option<String>>("mime_in_reply_to")?
|
||||
.and_then(|in_reply_to| parse_message_id(&in_reply_to).ok()),
|
||||
@@ -565,10 +580,12 @@ impl Message {
|
||||
original_msg_id: row.get("original_msg_id")?,
|
||||
mime_modified: row.get("mime_modified")?,
|
||||
text,
|
||||
additional_text: String::new(),
|
||||
subject: row.get("subject")?,
|
||||
param: row.get::<_, String>("param")?.parse().unwrap_or_default(),
|
||||
hidden: row.get("hidden")?,
|
||||
location_id: row.get("location")?,
|
||||
chat_visibility: row.get::<_, Option<_>>("visibility")?.unwrap_or_default(),
|
||||
chat_blocked: row
|
||||
.get::<_, Option<Blocked>>("blocked")?
|
||||
.unwrap_or_default(),
|
||||
@@ -579,9 +596,48 @@ impl Message {
|
||||
.await
|
||||
.with_context(|| format!("failed to load message {id} from the database"))?;
|
||||
|
||||
if let Some(msg) = &mut msg {
|
||||
msg.additional_text =
|
||||
Self::get_additional_text(context, msg.download_state, &msg.param).await?;
|
||||
}
|
||||
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
/// Returns additional text which is appended to the message's text field
|
||||
/// when it is loaded from the database.
|
||||
/// Currently this is used to add infomation to pre-messages of what the download will be and how large it is
|
||||
async fn get_additional_text(
|
||||
context: &Context,
|
||||
download_state: DownloadState,
|
||||
param: &Params,
|
||||
) -> Result<String> {
|
||||
if download_state != DownloadState::Done {
|
||||
let file_size = param
|
||||
.get(Param::PostMessageFileBytes)
|
||||
.and_then(|s| s.parse().ok())
|
||||
.map(|file_size: usize| format_size(file_size, BINARY))
|
||||
.unwrap_or("?".to_owned());
|
||||
let viewtype = param
|
||||
.get_i64(Param::PostMessageViewtype)
|
||||
.and_then(Viewtype::from_i64)
|
||||
.unwrap_or(Viewtype::Unknown);
|
||||
let file_name = param
|
||||
.get(Param::Filename)
|
||||
.map(sanitize_filename)
|
||||
.unwrap_or("?".to_owned());
|
||||
|
||||
return match viewtype {
|
||||
Viewtype::File => Ok(format!(" [{file_name} – {file_size}]")),
|
||||
_ => {
|
||||
let translated_viewtype = viewtype.to_locale_string(context).await;
|
||||
Ok(format!(" [{translated_viewtype} – {file_size}]"))
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(String::new())
|
||||
}
|
||||
|
||||
/// Returns the MIME type of an attached file if it exists.
|
||||
///
|
||||
/// If the MIME type is not known, the function guesses the MIME type
|
||||
@@ -763,8 +819,11 @@ impl Message {
|
||||
}
|
||||
|
||||
/// Returns the text of the message.
|
||||
///
|
||||
/// Currently this includes `additional_text`, but this may change in future, when the UIs show
|
||||
/// the necessary info themselves.
|
||||
pub fn get_text(&self) -> String {
|
||||
self.text.clone()
|
||||
self.text.clone() + &self.additional_text
|
||||
}
|
||||
|
||||
/// Returns message subject.
|
||||
@@ -786,7 +845,16 @@ impl Message {
|
||||
}
|
||||
|
||||
/// Returns the size of the file in bytes, if applicable.
|
||||
/// If message is a pre-message, then this returns the size of the file to be downloaded.
|
||||
pub async fn get_filebytes(&self, context: &Context) -> Result<Option<u64>> {
|
||||
if self.download_state != DownloadState::Done
|
||||
&& let Some(file_size) = self
|
||||
.param
|
||||
.get(Param::PostMessageFileBytes)
|
||||
.and_then(|s| s.parse().ok())
|
||||
{
|
||||
return Ok(Some(file_size));
|
||||
}
|
||||
if let Some(path) = self.param.get_file_path(context)? {
|
||||
Ok(Some(get_filebytes(context, &path).await.with_context(
|
||||
|| format!("failed to get {} size in bytes", path.display()),
|
||||
@@ -796,6 +864,19 @@ impl Message {
|
||||
}
|
||||
}
|
||||
|
||||
/// If message is a Pre-Message,
|
||||
/// then this returns the viewtype it will have when it is downloaded.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn get_post_message_viewtype(&self) -> Option<Viewtype> {
|
||||
if self.download_state != DownloadState::Done {
|
||||
return self
|
||||
.param
|
||||
.get_i64(Param::PostMessageViewtype)
|
||||
.and_then(Viewtype::from_i64);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns width of associated image or video file.
|
||||
pub fn get_width(&self) -> i32 {
|
||||
self.param.get_int(Param::Width).unwrap_or_default()
|
||||
@@ -845,11 +926,10 @@ impl Message {
|
||||
|
||||
let contact = if self.from_id != ContactId::SELF {
|
||||
match chat.typ {
|
||||
Chattype::Group
|
||||
| Chattype::OutBroadcast
|
||||
| Chattype::InBroadcast
|
||||
| Chattype::Mailinglist => Some(Contact::get_by_id(context, self.from_id).await?),
|
||||
Chattype::Single => None,
|
||||
Chattype::Group | Chattype::Mailinglist => {
|
||||
Some(Contact::get_by_id(context, self.from_id).await?)
|
||||
}
|
||||
Chattype::Single | Chattype::OutBroadcast | Chattype::InBroadcast => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
@@ -900,7 +980,7 @@ impl Message {
|
||||
|
||||
/// Returns true if the message is a forwarded message.
|
||||
pub fn is_forwarded(&self) -> bool {
|
||||
0 != self.param.get_int(Param::Forwarded).unwrap_or_default()
|
||||
self.param.get_int(Param::Forwarded).is_some()
|
||||
}
|
||||
|
||||
/// Returns true if the message is edited.
|
||||
@@ -1426,6 +1506,16 @@ pub async fn get_msg_read_receipts(
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns count of read receipts on message.
|
||||
///
|
||||
/// This view count is meant as a feedback measure for the channel owner only.
|
||||
pub async fn get_msg_read_receipt_count(context: &Context, msg_id: MsgId) -> Result<usize> {
|
||||
context
|
||||
.sql
|
||||
.count("SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=?", (msg_id,))
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) fn guess_msgtype_from_suffix(msg: &Message) -> Option<(Viewtype, &'static str)> {
|
||||
msg.param
|
||||
.get(Param::Filename)
|
||||
@@ -1672,13 +1762,21 @@ pub async fn delete_msgs_ex(
|
||||
modified_chat_ids.insert(msg.chat_id);
|
||||
deleted_rfc724_mid.push(msg.rfc724_mid.clone());
|
||||
|
||||
let target = context.get_delete_msgs_target().await?;
|
||||
let update_db = |trans: &mut rusqlite::Transaction| {
|
||||
trans.execute(
|
||||
"UPDATE imap SET target=? WHERE rfc724_mid=?",
|
||||
(target, msg.rfc724_mid),
|
||||
)?;
|
||||
let mut stmt = trans.prepare("UPDATE imap SET target='' WHERE rfc724_mid=?")?;
|
||||
stmt.execute((&msg.rfc724_mid,))?;
|
||||
if !msg.pre_rfc724_mid.is_empty() {
|
||||
stmt.execute((&msg.pre_rfc724_mid,))?;
|
||||
}
|
||||
trans.execute("DELETE FROM smtp WHERE msg_id=?", (msg_id,))?;
|
||||
trans.execute(
|
||||
"DELETE FROM download WHERE rfc724_mid=?",
|
||||
(&msg.rfc724_mid,),
|
||||
)?;
|
||||
trans.execute(
|
||||
"DELETE FROM available_post_msgs WHERE rfc724_mid=?",
|
||||
(&msg.rfc724_mid,),
|
||||
)?;
|
||||
Ok(())
|
||||
};
|
||||
if let Err(e) = context.sql.transaction(update_db).await {
|
||||
@@ -1712,6 +1810,7 @@ pub async fn delete_msgs_ex(
|
||||
msgs: deleted_rfc724_mid,
|
||||
})
|
||||
.await?;
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
}
|
||||
|
||||
for &msg_id in msg_ids {
|
||||
@@ -1746,11 +1845,11 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
"SELECT
|
||||
m.chat_id AS chat_id,
|
||||
m.state AS state,
|
||||
m.download_state as download_state,
|
||||
m.ephemeral_timer AS ephemeral_timer,
|
||||
m.param AS param,
|
||||
m.from_id AS from_id,
|
||||
m.rfc724_mid AS rfc724_mid,
|
||||
m.hidden AS hidden,
|
||||
c.archived AS archived,
|
||||
c.blocked AS blocked
|
||||
FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id
|
||||
@@ -1759,10 +1858,10 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
|row| {
|
||||
let chat_id: ChatId = row.get("chat_id")?;
|
||||
let state: MessageState = row.get("state")?;
|
||||
let download_state: DownloadState = row.get("download_state")?;
|
||||
let param: Params = row.get::<_, String>("param")?.parse().unwrap_or_default();
|
||||
let from_id: ContactId = row.get("from_id")?;
|
||||
let rfc724_mid: String = row.get("rfc724_mid")?;
|
||||
let hidden: bool = row.get("hidden")?;
|
||||
let visibility: ChatVisibility = row.get("archived")?;
|
||||
let blocked: Option<Blocked> = row.get("blocked")?;
|
||||
let ephemeral_timer: EphemeralTimer = row.get("ephemeral_timer")?;
|
||||
@@ -1771,10 +1870,10 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
id,
|
||||
chat_id,
|
||||
state,
|
||||
download_state,
|
||||
param,
|
||||
from_id,
|
||||
rfc724_mid,
|
||||
hidden,
|
||||
visibility,
|
||||
blocked.unwrap_or_default(),
|
||||
),
|
||||
@@ -1804,31 +1903,25 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
id,
|
||||
curr_chat_id,
|
||||
curr_state,
|
||||
curr_download_state,
|
||||
curr_param,
|
||||
curr_from_id,
|
||||
curr_rfc724_mid,
|
||||
curr_hidden,
|
||||
curr_visibility,
|
||||
curr_blocked,
|
||||
),
|
||||
_curr_ephemeral_timer,
|
||||
) in msgs
|
||||
{
|
||||
if curr_download_state != DownloadState::Done {
|
||||
if curr_state == MessageState::InFresh {
|
||||
// Don't mark partially downloaded messages as seen or send a read receipt since
|
||||
// they are not really seen by the user.
|
||||
update_msg_state(context, id, MessageState::InNoticed).await?;
|
||||
updated_chat_ids.insert(curr_chat_id);
|
||||
}
|
||||
} else if curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed {
|
||||
if curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed {
|
||||
update_msg_state(context, id, MessageState::InSeen).await?;
|
||||
info!(context, "Seen message {}.", id);
|
||||
|
||||
markseen_on_imap_table(context, &curr_rfc724_mid).await?;
|
||||
|
||||
// Read receipts for system messages are never sent. These messages have no place to
|
||||
// display received read receipt anyway. And since their text is locally generated,
|
||||
// Read receipts for system messages are never sent to contacts.
|
||||
// These messages have no place to display received read receipt
|
||||
// anyway. And since their text is locally generated,
|
||||
// quoting them is dangerous as it may contain contact names. E.g., for original message
|
||||
// "Group left by me", a read receipt will quote "Group left by <name>", and the name can
|
||||
// be a display name stored in address book rather than the name sent in the From field by
|
||||
@@ -1836,25 +1929,35 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
//
|
||||
// We also don't send read receipts for contact requests.
|
||||
// Read receipts will not be sent even after accepting the chat.
|
||||
if curr_blocked == Blocked::Not
|
||||
let to_id = if curr_blocked == Blocked::Not
|
||||
&& curr_param.get_bool(Param::WantsMdn).unwrap_or_default()
|
||||
&& curr_param.get_cmd() == SystemMessage::Unknown
|
||||
&& context.should_send_mdns().await?
|
||||
{
|
||||
Some(curr_from_id)
|
||||
} else if context.get_config_bool(Config::BccSelf).await? {
|
||||
Some(ContactId::SELF)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(to_id) = to_id {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO smtp_mdns (msg_id, from_id, rfc724_mid) VALUES(?, ?, ?)",
|
||||
(id, curr_from_id, curr_rfc724_mid),
|
||||
(id, to_id, curr_rfc724_mid),
|
||||
)
|
||||
.await
|
||||
.context("failed to insert into smtp_mdns")?;
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
}
|
||||
updated_chat_ids.insert(curr_chat_id);
|
||||
if !curr_hidden {
|
||||
updated_chat_ids.insert(curr_chat_id);
|
||||
}
|
||||
}
|
||||
archived_chats_maybe_noticed |=
|
||||
curr_state == MessageState::InFresh && curr_visibility == ChatVisibility::Archived;
|
||||
archived_chats_maybe_noticed |= curr_state == MessageState::InFresh
|
||||
&& !curr_hidden
|
||||
&& curr_visibility == ChatVisibility::Archived;
|
||||
}
|
||||
|
||||
for updated_chat_id in updated_chat_ids {
|
||||
@@ -2092,7 +2195,7 @@ pub(crate) async fn rfc724_mid_exists_ex(
|
||||
.query_row_optional(
|
||||
&("SELECT id, timestamp_sent, MIN(".to_string()
|
||||
+ expr
|
||||
+ ") FROM msgs WHERE rfc724_mid=?
|
||||
+ ") FROM msgs WHERE rfc724_mid=?1 OR pre_rfc724_mid=?1
|
||||
HAVING COUNT(*) > 0 -- Prevent MIN(expr) from returning NULL when there are no rows.
|
||||
ORDER BY timestamp_sent DESC"),
|
||||
(rfc724_mid,),
|
||||
@@ -2107,6 +2210,32 @@ pub(crate) async fn rfc724_mid_exists_ex(
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Returns `true` iff there is a message
|
||||
/// with the given `rfc724_mid`
|
||||
/// and a download state other than `DownloadState::Available`,
|
||||
/// i.e. it was already tried to download the message or it's sent locally.
|
||||
pub(crate) async fn rfc724_mid_download_tried(context: &Context, rfc724_mid: &str) -> Result<bool> {
|
||||
let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>');
|
||||
if rfc724_mid.is_empty() {
|
||||
warn!(
|
||||
context,
|
||||
"Empty rfc724_mid passed to rfc724_mid_download_tried"
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let res = context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM msgs
|
||||
WHERE rfc724_mid=? AND download_state<>?",
|
||||
(rfc724_mid, DownloadState::Available),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Given a list of Message-IDs, returns the most relevant message found in the database.
|
||||
///
|
||||
/// Relevance here is `(download_state == Done, index)`, where `index` is an index of Message-ID in
|
||||
|
||||
@@ -326,79 +326,7 @@ async fn test_markseen_msgs() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_markseen_not_downloaded_msg() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
alice.set_config(Config::DownloadLimit, Some("1")).await?;
|
||||
let bob = &tcm.bob().await;
|
||||
let bob_chat_id = bob.create_chat(alice).await.id;
|
||||
alice.create_chat(bob).await; // Make sure the chat is accepted.
|
||||
|
||||
tcm.section("Bob sends a large message to Alice");
|
||||
let file_bytes = include_bytes!("../../test-data/image/screenshot.png");
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file_from_bytes(bob, "a.jpg", file_bytes, None)?;
|
||||
let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await;
|
||||
|
||||
tcm.section("Alice receives a large message from Bob");
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.download_state, DownloadState::Available);
|
||||
assert!(!msg.param.get_bool(Param::WantsMdn).unwrap_or_default());
|
||||
assert_eq!(msg.state, MessageState::InFresh);
|
||||
markseen_msgs(alice, vec![msg.id]).await?;
|
||||
// A not downloaded message can be seen only if it's seen on another device.
|
||||
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
|
||||
// Marking the message as seen again is a no op.
|
||||
markseen_msgs(alice, vec![msg.id]).await?;
|
||||
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
|
||||
|
||||
msg.id
|
||||
.update_download_state(alice, DownloadState::InProgress)
|
||||
.await?;
|
||||
markseen_msgs(alice, vec![msg.id]).await?;
|
||||
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
|
||||
msg.id
|
||||
.update_download_state(alice, DownloadState::Failure)
|
||||
.await?;
|
||||
markseen_msgs(alice, vec![msg.id]).await?;
|
||||
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
|
||||
msg.id
|
||||
.update_download_state(alice, DownloadState::Undecipherable)
|
||||
.await?;
|
||||
markseen_msgs(alice, vec![msg.id]).await?;
|
||||
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
|
||||
|
||||
assert!(
|
||||
!alice
|
||||
.sql
|
||||
.exists("SELECT COUNT(*) FROM smtp_mdns", ())
|
||||
.await?
|
||||
);
|
||||
|
||||
alice.set_config(Config::DownloadLimit, None).await?;
|
||||
// Let's assume that Alice and Bob resolved the problem with encryption.
|
||||
let old_msg = msg;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.chat_id, old_msg.chat_id);
|
||||
assert_eq!(msg.download_state, DownloadState::Done);
|
||||
assert!(msg.param.get_bool(Param::WantsMdn).unwrap_or_default());
|
||||
assert!(msg.get_showpadlock());
|
||||
// The message state mustn't be downgraded to `InFresh`.
|
||||
assert_eq!(msg.state, MessageState::InNoticed);
|
||||
markseen_msgs(alice, vec![msg.id]).await?;
|
||||
let msg = Message::load_from_db(alice, msg.id).await?;
|
||||
assert_eq!(msg.state, MessageState::InSeen);
|
||||
assert_eq!(
|
||||
alice
|
||||
.sql
|
||||
.count("SELECT COUNT(*) FROM smtp_mdns", ())
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Message has been seen on another device when fully downloaded. `state` should be updated.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_msg_seen_on_imap_when_downloaded() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -411,20 +339,17 @@ async fn test_msg_seen_on_imap_when_downloaded() -> Result<()> {
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file_from_bytes(bob, "a.jpg", file_bytes, None)?;
|
||||
let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
let pre_msg = bob.pop_sent_msg().await;
|
||||
let msg = alice.recv_msg(&pre_msg).await;
|
||||
assert_eq!(msg.download_state, DownloadState::Available);
|
||||
assert_eq!(msg.state, MessageState::InFresh);
|
||||
|
||||
alice.set_config(Config::DownloadLimit, None).await?;
|
||||
let seen = true;
|
||||
let rcvd_msg = receive_imf(alice, sent_msg.payload().as_bytes(), seen)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(rcvd_msg.chat_id, msg.chat_id);
|
||||
let msg = Message::load_from_db(alice, *rcvd_msg.msg_ids.last().unwrap())
|
||||
.await
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_eq!(rcvd_msg.chat_id, DC_CHAT_ID_TRASH);
|
||||
let msg = Message::load_from_db(alice, msg.id).await?;
|
||||
assert_eq!(msg.download_state, DownloadState::Done);
|
||||
assert!(msg.param.get_bool(Param::WantsMdn).unwrap_or_default());
|
||||
assert!(msg.get_showpadlock());
|
||||
@@ -432,6 +357,60 @@ async fn test_msg_seen_on_imap_when_downloaded() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_pre_and_post_msgs_deleted() -> Result<()> {
|
||||
let reorder = false;
|
||||
test_pre_and_post_msgs_deleted_ex(reorder).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_reordered_pre_and_post_msgs_deleted() -> Result<()> {
|
||||
let reorder = true;
|
||||
test_pre_and_post_msgs_deleted_ex(reorder).await
|
||||
}
|
||||
|
||||
async fn test_pre_and_post_msgs_deleted_ex(reorder: bool) -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_chat_id = alice.create_group_with_members("", &[bob]).await;
|
||||
|
||||
let file_bytes = include_bytes!("../../test-data/image/screenshot.gif");
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file_from_bytes(alice, "a.jpg", file_bytes, None)?;
|
||||
let full_msg = alice.send_msg(alice_chat_id, &mut msg).await;
|
||||
let pre_msg = alice.pop_sent_msg().await;
|
||||
|
||||
let rfc724_mid_pre = bob.parse_msg(&pre_msg).await.get_rfc724_mid().unwrap();
|
||||
let msg = if reorder {
|
||||
let msg = bob.recv_msg(&full_msg).await;
|
||||
bob.recv_msg_trash(&pre_msg).await;
|
||||
Message::load_from_db(bob, msg.id).await?
|
||||
} else {
|
||||
let msg = bob.recv_msg(&pre_msg).await;
|
||||
bob.recv_msg_trash(&full_msg).await;
|
||||
msg
|
||||
};
|
||||
assert_ne!(rfc724_mid_pre, msg.rfc724_mid);
|
||||
for (rfc724_mid, uid) in [(&rfc724_mid_pre, 1), (&msg.rfc724_mid, 2)] {
|
||||
bob.sql
|
||||
.execute(
|
||||
"INSERT INTO imap (transport_id, rfc724_mid, folder, uid, target, uidvalidity) VALUES (1, ?, 'INBOX', ?, 'INBOX', 12345)",
|
||||
(rfc724_mid, uid),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
delete_msgs(bob, &[msg.id]).await?;
|
||||
assert_eq!(
|
||||
bob.sql
|
||||
.count("SELECT COUNT(*) FROM imap WHERE target!=''", ())
|
||||
.await?,
|
||||
0
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_state() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
@@ -21,6 +21,7 @@ use crate::constants::{ASM_SUBJECT, BROADCAST_INCOMPATIBILITY_MSG};
|
||||
use crate::constants::{Chattype, DC_FROM_HANDSHAKE};
|
||||
use crate::contact::{Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::download::PostMsgMetadata;
|
||||
use crate::e2ee::EncryptHelper;
|
||||
use crate::ensure_and_debug_assert;
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
@@ -59,6 +60,17 @@ pub enum Loaded {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum PreMessageMode {
|
||||
/// adds the Chat-Is-Post-Message header in unprotected part
|
||||
Post,
|
||||
/// adds the Chat-Post-Message-ID header to protected part
|
||||
/// also adds metadata and explicitly excludes attachment
|
||||
Pre { post_msg_rfc724_mid: String },
|
||||
/// Atomic ("normal") message.
|
||||
None,
|
||||
}
|
||||
|
||||
/// Helper to construct mime messages.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MimeFactory {
|
||||
@@ -94,6 +106,7 @@ pub struct MimeFactory {
|
||||
/// addresses and OpenPGP keys
|
||||
/// to use for encryption.
|
||||
///
|
||||
/// If `Some`, encrypt to self also.
|
||||
/// `None` if the message is not encrypted.
|
||||
encryption_pubkeys: Option<Vec<(String, SignedPublicKey)>>,
|
||||
|
||||
@@ -146,6 +159,9 @@ pub struct MimeFactory {
|
||||
|
||||
/// This field is used to sustain the topic id of webxdcs needed for peer channels.
|
||||
webxdc_topic: Option<TopicId>,
|
||||
|
||||
/// Pre-message / post-message / atomic message.
|
||||
pre_message_mode: PreMessageMode,
|
||||
}
|
||||
|
||||
/// Result of rendering a message, ready to be submitted to a send job.
|
||||
@@ -219,7 +235,6 @@ impl MimeFactory {
|
||||
encryption_pubkeys = if msg.param.get_bool(Param::ForcePlaintext).unwrap_or(false) {
|
||||
None
|
||||
} else {
|
||||
// Encrypt, but only to self.
|
||||
Some(Vec::new())
|
||||
};
|
||||
} else if chat.is_mailing_list() {
|
||||
@@ -232,6 +247,37 @@ impl MimeFactory {
|
||||
|
||||
// Do not encrypt messages to mailing lists.
|
||||
encryption_pubkeys = None;
|
||||
} else if let Some(fp) = must_have_only_one_recipient(&msg, &chat) {
|
||||
let fp = fp?;
|
||||
// In a broadcast channel, only send member-added/removed messages
|
||||
// to the affected member
|
||||
let (authname, addr) = context
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT authname, addr FROM contacts WHERE fingerprint=?",
|
||||
(fp,),
|
||||
|row| {
|
||||
let authname: String = row.get(0)?;
|
||||
let addr: String = row.get(1)?;
|
||||
Ok((authname, addr))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let public_key_bytes: Vec<_> = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT public_key FROM public_keys WHERE fingerprint=?",
|
||||
(fp,),
|
||||
)
|
||||
.await?
|
||||
.context("Can't send member addition/removal: missing key")?;
|
||||
|
||||
recipients.push(addr.clone());
|
||||
to.push((authname, addr.clone()));
|
||||
|
||||
let public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
|
||||
encryption_pubkeys = Some(vec![(addr, public_key)]);
|
||||
} else {
|
||||
let email_to_remove = if msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup {
|
||||
msg.param.get(Param::Arg)
|
||||
@@ -291,13 +337,6 @@ impl MimeFactory {
|
||||
for row in rows {
|
||||
let (authname, addr, fingerprint, id, add_timestamp, remove_timestamp, public_key_bytes_opt) = row?;
|
||||
|
||||
// In a broadcast channel, only send member-added/removed messages
|
||||
// to the affected member:
|
||||
if let Some(fp) = must_have_only_one_recipient(&msg, &chat)
|
||||
&& fp? != fingerprint {
|
||||
continue;
|
||||
}
|
||||
|
||||
let public_key_opt = if let Some(public_key_bytes) = &public_key_bytes_opt {
|
||||
Some(SignedPublicKey::from_slice(public_key_bytes)?)
|
||||
} else {
|
||||
@@ -411,8 +450,16 @@ impl MimeFactory {
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let recipient_ids: Vec<_> = recipient_ids.into_iter().collect();
|
||||
ContactId::scaleup_origin(context, &recipient_ids, Origin::OutgoingTo).await?;
|
||||
let recipient_ids: Vec<_> = recipient_ids
|
||||
.into_iter()
|
||||
.filter(|id| *id != ContactId::SELF)
|
||||
.collect();
|
||||
if recipient_ids.len() == 1
|
||||
&& msg.param.get_cmd() != SystemMessage::MemberRemovedFromGroup
|
||||
&& chat.typ != Chattype::OutBroadcast
|
||||
{
|
||||
ContactId::scaleup_origin(context, &recipient_ids, Origin::OutgoingTo).await?;
|
||||
}
|
||||
|
||||
if !msg.is_system_message()
|
||||
&& msg.param.get_int(Param::Reaction).unwrap_or_default() == 0
|
||||
@@ -500,6 +547,7 @@ impl MimeFactory {
|
||||
sync_ids_to_delete: None,
|
||||
attach_selfavatar,
|
||||
webxdc_topic,
|
||||
pre_message_mode: PreMessageMode::None,
|
||||
};
|
||||
Ok(factory)
|
||||
}
|
||||
@@ -515,7 +563,9 @@ impl MimeFactory {
|
||||
let timestamp = create_smeared_timestamp(context);
|
||||
|
||||
let addr = contact.get_addr().to_string();
|
||||
let encryption_pubkeys = if contact.is_key_contact() {
|
||||
let encryption_pubkeys = if from_id == ContactId::SELF {
|
||||
Some(Vec::new())
|
||||
} else if contact.is_key_contact() {
|
||||
if let Some(key) = contact.public_key(context).await? {
|
||||
Some(vec![(addr.clone(), key)])
|
||||
} else {
|
||||
@@ -548,6 +598,7 @@ impl MimeFactory {
|
||||
sync_ids_to_delete: None,
|
||||
attach_selfavatar: false,
|
||||
webxdc_topic: None,
|
||||
pre_message_mode: PreMessageMode::None,
|
||||
};
|
||||
|
||||
Ok(res)
|
||||
@@ -779,7 +830,10 @@ impl MimeFactory {
|
||||
headers.push(("Date", mail_builder::headers::raw::Raw::new(date).into()));
|
||||
|
||||
let rfc724_mid = match &self.loaded {
|
||||
Loaded::Message { msg, .. } => msg.rfc724_mid.clone(),
|
||||
Loaded::Message { msg, .. } => match &self.pre_message_mode {
|
||||
PreMessageMode::Pre { .. } => create_outgoing_rfc724_mid(),
|
||||
_ => msg.rfc724_mid.clone(),
|
||||
},
|
||||
Loaded::Mdn { .. } => create_outgoing_rfc724_mid(),
|
||||
};
|
||||
headers.push((
|
||||
@@ -893,7 +947,7 @@ impl MimeFactory {
|
||||
));
|
||||
}
|
||||
|
||||
let is_encrypted = self.encryption_pubkeys.is_some();
|
||||
let is_encrypted = self.will_be_encrypted();
|
||||
|
||||
// Add ephemeral timer for non-MDN messages.
|
||||
// For MDNs it does not matter because they are not visible
|
||||
@@ -978,6 +1032,23 @@ impl MimeFactory {
|
||||
"MIME-Version",
|
||||
mail_builder::headers::raw::Raw::new("1.0").into(),
|
||||
));
|
||||
|
||||
if self.pre_message_mode == PreMessageMode::Post {
|
||||
unprotected_headers.push((
|
||||
"Chat-Is-Post-Message",
|
||||
mail_builder::headers::raw::Raw::new("1").into(),
|
||||
));
|
||||
} else if let PreMessageMode::Pre {
|
||||
post_msg_rfc724_mid,
|
||||
} = &self.pre_message_mode
|
||||
{
|
||||
protected_headers.push((
|
||||
"Chat-Post-Message-ID",
|
||||
mail_builder::headers::message_id::MessageId::new(post_msg_rfc724_mid.clone())
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
|
||||
for header @ (original_header_name, _header_value) in &headers {
|
||||
let header_name = original_header_name.to_lowercase();
|
||||
if header_name == "message-id" {
|
||||
@@ -1119,6 +1190,10 @@ impl MimeFactory {
|
||||
for (addr, key) in &encryption_pubkeys {
|
||||
let fingerprint = key.dc_fingerprint().hex();
|
||||
let cmd = msg.param.get_cmd();
|
||||
if self.pre_message_mode == PreMessageMode::Post {
|
||||
continue;
|
||||
}
|
||||
|
||||
let should_do_gossip = cmd == SystemMessage::MemberAddedToGroup
|
||||
|| cmd == SystemMessage::SecurejoinMessage
|
||||
|| multiple_recipients && {
|
||||
@@ -1765,6 +1840,12 @@ impl MimeFactory {
|
||||
mail_builder::headers::raw::Raw::new(b_encode(answer)).into(),
|
||||
));
|
||||
}
|
||||
if let Some(has_video) = msg.param.get(Param::WebrtcHasVideoInitially) {
|
||||
headers.push((
|
||||
"Chat-Webrtc-Has-Video-Initially",
|
||||
mail_builder::headers::raw::Raw::new(b_encode(has_video)).into(),
|
||||
))
|
||||
}
|
||||
|
||||
if msg.viewtype == Viewtype::Voice
|
||||
|| msg.viewtype == Viewtype::Audio
|
||||
@@ -1831,19 +1912,23 @@ impl MimeFactory {
|
||||
|
||||
let footer = if is_reaction { "" } else { &self.selfstatus };
|
||||
|
||||
let message_text = format!(
|
||||
"{}{}{}{}{}{}",
|
||||
fwdhint.unwrap_or_default(),
|
||||
quoted_text.unwrap_or_default(),
|
||||
escape_message_footer_marks(final_text),
|
||||
if !final_text.is_empty() && !footer.is_empty() {
|
||||
"\r\n\r\n"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
if !footer.is_empty() { "-- \r\n" } else { "" },
|
||||
footer
|
||||
);
|
||||
let message_text = if self.pre_message_mode == PreMessageMode::Post {
|
||||
"".to_string()
|
||||
} else {
|
||||
format!(
|
||||
"{}{}{}{}{}{}",
|
||||
fwdhint.unwrap_or_default(),
|
||||
quoted_text.unwrap_or_default(),
|
||||
escape_message_footer_marks(final_text),
|
||||
if !final_text.is_empty() && !footer.is_empty() {
|
||||
"\r\n\r\n"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
if !footer.is_empty() { "-- \r\n" } else { "" },
|
||||
footer
|
||||
)
|
||||
};
|
||||
|
||||
let mut main_part = MimePart::new("text/plain", message_text);
|
||||
if is_reaction {
|
||||
@@ -1855,15 +1940,19 @@ impl MimeFactory {
|
||||
|
||||
let mut parts = Vec::new();
|
||||
|
||||
// add HTML-part, this is needed only if a HTML-message from a non-delta-client is forwarded;
|
||||
// for simplificity and to avoid conversion errors, we're generating the HTML-part from the original message.
|
||||
if msg.has_html() {
|
||||
let html = if let Some(orig_msg_id) = msg.param.get_int(Param::Forwarded) {
|
||||
let html = if let Some(html) = msg.param.get(Param::SendHtml) {
|
||||
Some(html.to_string())
|
||||
} else if let Some(orig_msg_id) = msg.param.get_int(Param::Forwarded)
|
||||
&& orig_msg_id != 0
|
||||
{
|
||||
// Legacy forwarded messages may not have `Param::SendHtml` set. Let's hope the
|
||||
// original message exists.
|
||||
MsgId::new(orig_msg_id.try_into()?)
|
||||
.get_html(context)
|
||||
.await?
|
||||
} else {
|
||||
msg.param.get(Param::SendHtml).map(|s| s.to_string())
|
||||
None
|
||||
};
|
||||
if let Some(html) = html {
|
||||
main_part = MimePart::new(
|
||||
@@ -1875,8 +1964,19 @@ impl MimeFactory {
|
||||
|
||||
// add attachment part
|
||||
if msg.viewtype.has_file() {
|
||||
let file_part = build_body_file(context, &msg).await?;
|
||||
parts.push(file_part);
|
||||
if let PreMessageMode::Pre { .. } = self.pre_message_mode {
|
||||
let Some(metadata) = PostMsgMetadata::from_msg(context, &msg).await? else {
|
||||
bail!("Failed to generate metadata for pre-message")
|
||||
};
|
||||
|
||||
headers.push((
|
||||
HeaderDef::ChatPostMessageMetadata.into(),
|
||||
mail_builder::headers::raw::Raw::new(metadata.to_header_value()?).into(),
|
||||
));
|
||||
} else {
|
||||
let file_part = build_body_file(context, &msg).await?;
|
||||
parts.push(file_part);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(msg_kml_part) = self.get_message_kml_part() {
|
||||
@@ -1921,6 +2021,8 @@ impl MimeFactory {
|
||||
}
|
||||
}
|
||||
|
||||
self.attach_selfavatar =
|
||||
self.attach_selfavatar && self.pre_message_mode != PreMessageMode::Post;
|
||||
if self.attach_selfavatar {
|
||||
match context.get_config(Config::Selfavatar).await? {
|
||||
Some(path) => match build_avatar_file(context, &path).await {
|
||||
@@ -1990,6 +2092,20 @@ impl MimeFactory {
|
||||
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
pub fn will_be_encrypted(&self) -> bool {
|
||||
self.encryption_pubkeys.is_some()
|
||||
}
|
||||
|
||||
pub fn set_as_post_message(&mut self) {
|
||||
self.pre_message_mode = PreMessageMode::Post;
|
||||
}
|
||||
|
||||
pub fn set_as_pre_message_for(&mut self, post_message: &RenderedEmail) {
|
||||
self.pre_message_mode = PreMessageMode::Pre {
|
||||
post_msg_rfc724_mid: post_message.rfc724_mid.clone(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn hidden_recipients() -> Address<'static> {
|
||||
|
||||
@@ -559,7 +559,7 @@ async fn test_render_reply() {
|
||||
"1.0"
|
||||
);
|
||||
|
||||
let _mime_msg = MimeMessage::from_bytes(context, rendered_msg.message.as_bytes(), None)
|
||||
let _mime_msg = MimeMessage::from_bytes(context, rendered_msg.message.as_bytes())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -757,7 +757,7 @@ async fn test_protected_headers_directive() -> Result<()> {
|
||||
assert!(msg.get_showpadlock());
|
||||
assert!(sent.payload.contains("\r\nSubject: [...]\r\n"));
|
||||
|
||||
let mime = MimeMessage::from_bytes(&alice, sent.payload.as_bytes(), None).await?;
|
||||
let mime = MimeMessage::from_bytes(&alice, sent.payload.as_bytes()).await?;
|
||||
let mut payload = str::from_utf8(&mime.decoded_data)?.splitn(2, "\r\n\r\n");
|
||||
let part = payload.next().unwrap();
|
||||
assert_eq!(
|
||||
@@ -781,7 +781,7 @@ async fn test_hp_outer_headers() -> Result<()> {
|
||||
.await?;
|
||||
chat::send_text_msg(t, chat_id, "hi!".to_string()).await?;
|
||||
let sent_msg = t.pop_sent_msg().await;
|
||||
let msg = MimeMessage::from_bytes(t, sent_msg.payload.as_bytes(), None).await?;
|
||||
let msg = MimeMessage::from_bytes(t, sent_msg.payload.as_bytes()).await?;
|
||||
assert_eq!(msg.header_exists(HeaderDef::HpOuter), std_hp_composing);
|
||||
for hdr in ["Date", "From", "Message-ID"] {
|
||||
assert_eq!(
|
||||
@@ -811,7 +811,7 @@ async fn test_dont_remove_self() -> Result<()> {
|
||||
.await;
|
||||
|
||||
println!("{}", sent.payload);
|
||||
let mime_message = MimeMessage::from_bytes(alice, sent.payload.as_bytes(), None)
|
||||
let mime_message = MimeMessage::from_bytes(alice, sent.payload.as_bytes())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!mime_message.header_exists(HeaderDef::ChatGroupPastMembers));
|
||||
|
||||
@@ -23,6 +23,7 @@ use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::decrypt::{try_decrypt, validate_detached_signature};
|
||||
use crate::dehtml::dehtml;
|
||||
use crate::download::PostMsgMetadata;
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::key::{self, DcKey, Fingerprint, SignedPublicKey, load_self_secret_keyring};
|
||||
@@ -88,11 +89,12 @@ pub(crate) struct MimeMessage {
|
||||
pub decrypting_failed: bool,
|
||||
|
||||
/// Valid signature fingerprint if a message is an
|
||||
/// Autocrypt encrypted and signed message.
|
||||
/// Autocrypt encrypted and signed message and corresponding intended recipient fingerprints
|
||||
/// (<https://www.rfc-editor.org/rfc/rfc9580.html#name-intended-recipient-fingerpr>) if any.
|
||||
///
|
||||
/// If a message is not encrypted or the signature is not valid,
|
||||
/// this is `None`.
|
||||
pub signature: Option<Fingerprint>,
|
||||
pub signature: Option<(Fingerprint, HashSet<Fingerprint>)>,
|
||||
|
||||
/// The addresses for which there was a gossip header
|
||||
/// and their respective gossiped keys.
|
||||
@@ -147,6 +149,25 @@ pub(crate) struct MimeMessage {
|
||||
/// Sender timestamp in secs since epoch. Allowed to be in the future due to unsynchronized
|
||||
/// clocks, but not too much.
|
||||
pub(crate) timestamp_sent: i64,
|
||||
|
||||
pub(crate) pre_message: PreMessageMode,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) enum PreMessageMode {
|
||||
/// This is a post-message.
|
||||
/// It replaces its pre-message attachment if it exists already,
|
||||
/// and if the pre-message does not exist, it is treated as a normal message.
|
||||
Post,
|
||||
/// This is a Pre-Message,
|
||||
/// it adds a message preview for a Post-Message
|
||||
/// and it is ignored if the Post-Message was downloaded already
|
||||
Pre {
|
||||
post_msg_rfc724_mid: String,
|
||||
metadata: Option<PostMsgMetadata>,
|
||||
},
|
||||
/// Atomic ("normal") message.
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
@@ -240,12 +261,9 @@ const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";
|
||||
impl MimeMessage {
|
||||
/// Parse a mime message.
|
||||
///
|
||||
/// If `partial` is set, it contains the full message size in bytes.
|
||||
pub(crate) async fn from_bytes(
|
||||
context: &Context,
|
||||
body: &[u8],
|
||||
partial: Option<u32>,
|
||||
) -> Result<Self> {
|
||||
/// This method has some side-effects,
|
||||
/// such as saving blobs and saving found public keys to the database.
|
||||
pub(crate) async fn from_bytes(context: &Context, body: &[u8]) -> Result<Self> {
|
||||
let mail = mailparse::parse_mail(body)?;
|
||||
|
||||
let timestamp_rcvd = smeared_time(context);
|
||||
@@ -302,7 +320,7 @@ impl MimeMessage {
|
||||
);
|
||||
(part, part.ctype.mimetype.parse::<Mime>()?)
|
||||
} else {
|
||||
// If it's a partially fetched message, there are no subparts.
|
||||
// Not a valid signed message, handle it as plaintext.
|
||||
(&mail, mimetype)
|
||||
}
|
||||
} else {
|
||||
@@ -352,6 +370,16 @@ impl MimeMessage {
|
||||
|
||||
let mut aheader_values = mail.headers.get_all_values(HeaderDef::Autocrypt.into());
|
||||
|
||||
let mut pre_message = if mail
|
||||
.headers
|
||||
.get_header_value(HeaderDef::ChatIsPostMessage)
|
||||
.is_some()
|
||||
{
|
||||
PreMessageMode::Post
|
||||
} else {
|
||||
PreMessageMode::None
|
||||
};
|
||||
|
||||
let mail_raw; // Memory location for a possible decrypted message.
|
||||
let decrypted_msg; // Decrypted signed OpenPGP message.
|
||||
let secrets: Vec<String> = context
|
||||
@@ -502,12 +530,16 @@ impl MimeMessage {
|
||||
let mut signatures = if let Some(ref decrypted_msg) = decrypted_msg {
|
||||
crate::pgp::valid_signature_fingerprints(decrypted_msg, &public_keyring)
|
||||
} else {
|
||||
HashSet::new()
|
||||
HashMap::new()
|
||||
};
|
||||
|
||||
let mail = mail.as_ref().map(|mail| {
|
||||
let (content, signatures_detached) = validate_detached_signature(mail, &public_keyring)
|
||||
.unwrap_or((mail, Default::default()));
|
||||
let signatures_detached = signatures_detached
|
||||
.into_iter()
|
||||
.map(|fp| (fp, Vec::new()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
signatures.extend(signatures_detached);
|
||||
content
|
||||
});
|
||||
@@ -580,6 +612,43 @@ impl MimeMessage {
|
||||
signatures.clear();
|
||||
}
|
||||
|
||||
if let (Ok(mail), true) = (mail, is_encrypted)
|
||||
&& let Some(post_msg_rfc724_mid) =
|
||||
mail.headers.get_header_value(HeaderDef::ChatPostMessageId)
|
||||
{
|
||||
let post_msg_rfc724_mid = parse_message_id(&post_msg_rfc724_mid)?;
|
||||
let metadata = if let Some(value) = mail
|
||||
.headers
|
||||
.get_header_value(HeaderDef::ChatPostMessageMetadata)
|
||||
{
|
||||
match PostMsgMetadata::try_from_header_value(&value) {
|
||||
Ok(metadata) => Some(metadata),
|
||||
Err(error) => {
|
||||
error!(
|
||||
context,
|
||||
"Failed to parse metadata header in pre-message for {post_msg_rfc724_mid}: {error:#}."
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"Expected pre-message for {post_msg_rfc724_mid} to have metadata header."
|
||||
);
|
||||
None
|
||||
};
|
||||
|
||||
pre_message = PreMessageMode::Pre {
|
||||
post_msg_rfc724_mid,
|
||||
metadata,
|
||||
};
|
||||
}
|
||||
|
||||
let signature = signatures
|
||||
.into_iter()
|
||||
.last()
|
||||
.map(|(fp, recipient_fps)| (fp, recipient_fps.into_iter().collect::<HashSet<_>>()));
|
||||
let mut parser = MimeMessage {
|
||||
parts: Vec::new(),
|
||||
headers,
|
||||
@@ -595,7 +664,7 @@ impl MimeMessage {
|
||||
decrypting_failed: mail.is_err(),
|
||||
|
||||
// only non-empty if it was a valid autocrypt message
|
||||
signature: signatures.into_iter().last(),
|
||||
signature,
|
||||
autocrypt_fingerprint,
|
||||
gossiped_keys,
|
||||
is_forwarded: false,
|
||||
@@ -615,33 +684,27 @@ impl MimeMessage {
|
||||
is_bot: None,
|
||||
timestamp_rcvd,
|
||||
timestamp_sent,
|
||||
pre_message,
|
||||
};
|
||||
|
||||
match partial {
|
||||
Some(org_bytes) => {
|
||||
parser
|
||||
.create_stub_from_partial_download(context, org_bytes)
|
||||
.await?;
|
||||
match mail {
|
||||
Ok(mail) => {
|
||||
parser.parse_mime_recursive(context, mail, false).await?;
|
||||
}
|
||||
None => match mail {
|
||||
Ok(mail) => {
|
||||
parser.parse_mime_recursive(context, mail, false).await?;
|
||||
}
|
||||
Err(err) => {
|
||||
let txt = "[This message cannot be decrypted.\n\n• It might already help to simply reply to this message and ask the sender to send the message again.\n\n• If you just re-installed Delta Chat then it is best if you re-setup Delta Chat now and choose \"Add as second device\" or import a backup.]";
|
||||
Err(err) => {
|
||||
let txt = "[This message cannot be decrypted.\n\n• It might already help to simply reply to this message and ask the sender to send the message again.\n\n• If you just re-installed Delta Chat then it is best if you re-setup Delta Chat now and choose \"Add as second device\" or import a backup.]";
|
||||
|
||||
let part = Part {
|
||||
typ: Viewtype::Text,
|
||||
msg_raw: Some(txt.to_string()),
|
||||
msg: txt.to_string(),
|
||||
// Don't change the error prefix for now,
|
||||
// receive_imf.rs:lookup_chat_by_reply() checks it.
|
||||
error: Some(format!("Decrypting failed: {err:#}")),
|
||||
..Default::default()
|
||||
};
|
||||
parser.do_add_single_part(part);
|
||||
}
|
||||
},
|
||||
let part = Part {
|
||||
typ: Viewtype::Text,
|
||||
msg_raw: Some(txt.to_string()),
|
||||
msg: txt.to_string(),
|
||||
// Don't change the error prefix for now,
|
||||
// receive_imf.rs:lookup_chat_by_reply() checks it.
|
||||
error: Some(format!("Decrypting failed: {err:#}")),
|
||||
..Default::default()
|
||||
};
|
||||
parser.do_add_single_part(part);
|
||||
}
|
||||
};
|
||||
|
||||
let is_location_only = parser.location_kml.is_some() && parser.parts.is_empty();
|
||||
@@ -738,6 +801,9 @@ impl MimeMessage {
|
||||
let accepted = self
|
||||
.get_header(HeaderDef::ChatWebrtcAccepted)
|
||||
.map(|s| s.to_string());
|
||||
let has_video = self
|
||||
.get_header(HeaderDef::ChatWebrtcHasVideoInitially)
|
||||
.map(|s| s.to_string());
|
||||
if let Some(part) = self.parts.first_mut() {
|
||||
if let Some(room) = room {
|
||||
if content == "call" {
|
||||
@@ -747,6 +813,9 @@ impl MimeMessage {
|
||||
} else if let Some(accepted) = accepted {
|
||||
part.param.set(Param::WebrtcAccepted, accepted);
|
||||
}
|
||||
if let Some(has_video) = has_video {
|
||||
part.param.set(Param::WebrtcHasVideoInitially, has_video);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2105,9 +2174,9 @@ pub(crate) struct Report {
|
||||
///
|
||||
/// It MUST be present if the original message has a Message-ID according to RFC 8098.
|
||||
/// In case we can't find it (shouldn't happen), this is None.
|
||||
original_message_id: Option<String>,
|
||||
pub original_message_id: Option<String>,
|
||||
/// Additional-Message-IDs
|
||||
additional_message_ids: Vec<String>,
|
||||
pub additional_message_ids: Vec<String>,
|
||||
}
|
||||
|
||||
/// Delivery Status Notification (RFC 3464, RFC 6533)
|
||||
@@ -2414,31 +2483,23 @@ async fn handle_mdn(
|
||||
timestamp_sent: i64,
|
||||
) -> Result<()> {
|
||||
if from_id == ContactId::SELF {
|
||||
warn!(
|
||||
context,
|
||||
"Ignoring MDN sent to self, this is a bug on the sender device."
|
||||
);
|
||||
|
||||
// This is not an error on our side,
|
||||
// we successfully ignored an invalid MDN and return `Ok`.
|
||||
// MDNs to self are handled in receive_imf_inner().
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some((msg_id, chat_id, has_mdns, is_dup)) = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
concat!(
|
||||
"SELECT",
|
||||
" m.id AS msg_id,",
|
||||
" c.id AS chat_id,",
|
||||
" mdns.contact_id AS mdn_contact",
|
||||
" FROM msgs m ",
|
||||
" LEFT JOIN chats c ON m.chat_id=c.id",
|
||||
" LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id",
|
||||
" WHERE rfc724_mid=? AND from_id=1",
|
||||
" ORDER BY msg_id DESC, mdn_contact=? DESC",
|
||||
" LIMIT 1",
|
||||
),
|
||||
"SELECT
|
||||
m.id AS msg_id,
|
||||
c.id AS chat_id,
|
||||
mdns.contact_id AS mdn_contact
|
||||
FROM msgs m
|
||||
LEFT JOIN chats c ON m.chat_id=c.id
|
||||
LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id
|
||||
WHERE rfc724_mid=? AND from_id=1
|
||||
ORDER BY msg_id DESC, mdn_contact=? DESC
|
||||
LIMIT 1",
|
||||
(&rfc724_mid, from_id),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get("msg_id")?;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use mailparse::ParsedMail;
|
||||
use std::mem;
|
||||
use std::time::Duration;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
chat,
|
||||
chatlist::Chatlist,
|
||||
constants::{self, Blocked, DC_DESIRED_TEXT_LEN, DC_ELLIPSIS},
|
||||
key,
|
||||
message::{MessageState, MessengerMessage},
|
||||
receive_imf::receive_imf,
|
||||
test_utils::{TestContext, TestContextManager},
|
||||
@@ -25,58 +27,54 @@ impl AvatarAction {
|
||||
async fn test_mimeparser_fromheader() {
|
||||
let ctx = TestContext::new_alice().await;
|
||||
|
||||
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: g@c.de\n\nhi", None)
|
||||
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: g@c.de\n\nhi")
|
||||
.await
|
||||
.unwrap();
|
||||
let contact = mimemsg.from;
|
||||
assert_eq!(contact.addr, "g@c.de");
|
||||
assert_eq!(contact.display_name, None);
|
||||
|
||||
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: g@c.de \n\nhi", None)
|
||||
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: g@c.de \n\nhi")
|
||||
.await
|
||||
.unwrap();
|
||||
let contact = mimemsg.from;
|
||||
assert_eq!(contact.addr, "g@c.de");
|
||||
assert_eq!(contact.display_name, None);
|
||||
|
||||
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: <g@c.de>\n\nhi", None)
|
||||
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: <g@c.de>\n\nhi")
|
||||
.await
|
||||
.unwrap();
|
||||
let contact = mimemsg.from;
|
||||
assert_eq!(contact.addr, "g@c.de");
|
||||
assert_eq!(contact.display_name, None);
|
||||
|
||||
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: Goetz C <g@c.de>\n\nhi", None)
|
||||
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: Goetz C <g@c.de>\n\nhi")
|
||||
.await
|
||||
.unwrap();
|
||||
let contact = mimemsg.from;
|
||||
assert_eq!(contact.addr, "g@c.de");
|
||||
assert_eq!(contact.display_name, Some("Goetz C".to_string()));
|
||||
|
||||
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: \"Goetz C\" <g@c.de>\n\nhi", None)
|
||||
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: \"Goetz C\" <g@c.de>\n\nhi")
|
||||
.await
|
||||
.unwrap();
|
||||
let contact = mimemsg.from;
|
||||
assert_eq!(contact.addr, "g@c.de");
|
||||
assert_eq!(contact.display_name, Some("Goetz C".to_string()));
|
||||
|
||||
let mimemsg =
|
||||
MimeMessage::from_bytes(&ctx, b"From: =?utf-8?q?G=C3=B6tz?= C <g@c.de>\n\nhi", None)
|
||||
.await
|
||||
.unwrap();
|
||||
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: =?utf-8?q?G=C3=B6tz?= C <g@c.de>\n\nhi")
|
||||
.await
|
||||
.unwrap();
|
||||
let contact = mimemsg.from;
|
||||
assert_eq!(contact.addr, "g@c.de");
|
||||
assert_eq!(contact.display_name, Some("Götz C".to_string()));
|
||||
|
||||
// although RFC 2047 says, encoded-words shall not appear inside quoted-string,
|
||||
// this combination is used in the wild eg. by MailMate
|
||||
let mimemsg = MimeMessage::from_bytes(
|
||||
&ctx,
|
||||
b"From: \"=?utf-8?q?G=C3=B6tz?= C\" <g@c.de>\n\nhi",
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let mimemsg =
|
||||
MimeMessage::from_bytes(&ctx, b"From: \"=?utf-8?q?G=C3=B6tz?= C\" <g@c.de>\n\nhi")
|
||||
.await
|
||||
.unwrap();
|
||||
let contact = mimemsg.from;
|
||||
assert_eq!(contact.addr, "g@c.de");
|
||||
assert_eq!(contact.display_name, Some("Götz C".to_string()));
|
||||
@@ -86,7 +84,7 @@ async fn test_mimeparser_fromheader() {
|
||||
async fn test_mimeparser_crash() {
|
||||
let context = TestContext::new_alice().await;
|
||||
let raw = include_bytes!("../../test-data/message/issue_523.txt");
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -98,7 +96,7 @@ async fn test_mimeparser_crash() {
|
||||
async fn test_get_rfc724_mid_exists() {
|
||||
let context = TestContext::new_alice().await;
|
||||
let raw = include_bytes!("../../test-data/message/mail_with_message_id.txt");
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -112,7 +110,7 @@ async fn test_get_rfc724_mid_exists() {
|
||||
async fn test_get_rfc724_mid_not_exists() {
|
||||
let context = TestContext::new_alice().await;
|
||||
let raw = include_bytes!("../../test-data/message/issue_523.txt");
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(mimeparser.get_rfc724_mid(), None);
|
||||
@@ -324,7 +322,7 @@ async fn test_mailparse_0_16_0_panic() {
|
||||
|
||||
// There should be an error, but no panic.
|
||||
assert!(
|
||||
MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||
MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
@@ -341,7 +339,7 @@ async fn test_parse_first_addr() {
|
||||
test1\n\
|
||||
";
|
||||
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None).await;
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]).await;
|
||||
|
||||
assert!(mimeparser.is_err());
|
||||
}
|
||||
@@ -356,7 +354,7 @@ async fn test_get_parent_timestamp() {
|
||||
\n\
|
||||
Some reply\n\
|
||||
";
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -402,7 +400,7 @@ async fn test_mimeparser_with_context() {
|
||||
--==break==--\n\
|
||||
\n";
|
||||
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -438,26 +436,26 @@ async fn test_mimeparser_with_avatars() {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
let raw = include_bytes!("../../test-data/message/mail_attach_txt.eml");
|
||||
let mimeparser = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap();
|
||||
let mimeparser = MimeMessage::from_bytes(&t, &raw[..]).await.unwrap();
|
||||
assert_eq!(mimeparser.user_avatar, None);
|
||||
assert_eq!(mimeparser.group_avatar, None);
|
||||
|
||||
let raw = include_bytes!("../../test-data/message/mail_with_user_avatar.eml");
|
||||
let mimeparser = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap();
|
||||
let mimeparser = MimeMessage::from_bytes(&t, &raw[..]).await.unwrap();
|
||||
assert_eq!(mimeparser.parts.len(), 1);
|
||||
assert_eq!(mimeparser.parts[0].typ, Viewtype::Text);
|
||||
assert!(mimeparser.user_avatar.unwrap().is_change());
|
||||
assert_eq!(mimeparser.group_avatar, None);
|
||||
|
||||
let raw = include_bytes!("../../test-data/message/mail_with_user_avatar_deleted.eml");
|
||||
let mimeparser = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap();
|
||||
let mimeparser = MimeMessage::from_bytes(&t, &raw[..]).await.unwrap();
|
||||
assert_eq!(mimeparser.parts.len(), 1);
|
||||
assert_eq!(mimeparser.parts[0].typ, Viewtype::Text);
|
||||
assert_eq!(mimeparser.user_avatar, Some(AvatarAction::Delete));
|
||||
assert_eq!(mimeparser.group_avatar, None);
|
||||
|
||||
let raw = include_bytes!("../../test-data/message/mail_with_user_and_group_avatars.eml");
|
||||
let mimeparser = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap();
|
||||
let mimeparser = MimeMessage::from_bytes(&t, &raw[..]).await.unwrap();
|
||||
assert_eq!(mimeparser.parts.len(), 1);
|
||||
assert_eq!(mimeparser.parts[0].typ, Viewtype::Text);
|
||||
assert!(mimeparser.user_avatar.unwrap().is_change());
|
||||
@@ -467,9 +465,7 @@ async fn test_mimeparser_with_avatars() {
|
||||
let raw = include_bytes!("../../test-data/message/mail_with_user_and_group_avatars.eml");
|
||||
let raw = String::from_utf8_lossy(raw).to_string();
|
||||
let raw = raw.replace("Chat-User-Avatar:", "Xhat-Xser-Xvatar:");
|
||||
let mimeparser = MimeMessage::from_bytes(&t, raw.as_bytes(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
let mimeparser = MimeMessage::from_bytes(&t, raw.as_bytes()).await.unwrap();
|
||||
assert_eq!(mimeparser.parts.len(), 1);
|
||||
assert_eq!(mimeparser.parts[0].typ, Viewtype::Image);
|
||||
assert_eq!(mimeparser.user_avatar, None);
|
||||
@@ -485,7 +481,7 @@ async fn test_mimeparser_with_videochat() {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
let raw = include_bytes!("../../test-data/message/videochat_invitation.eml");
|
||||
let mimeparser = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap();
|
||||
let mimeparser = MimeMessage::from_bytes(&t, &raw[..]).await.unwrap();
|
||||
assert_eq!(mimeparser.parts.len(), 1);
|
||||
assert_eq!(mimeparser.parts[0].typ, Viewtype::Text);
|
||||
assert_eq!(mimeparser.parts[0].param.get(Param::WebrtcRoom), None);
|
||||
@@ -528,7 +524,7 @@ Content-Disposition: attachment; filename=\"message.kml\"\n\
|
||||
--==break==--\n\
|
||||
;";
|
||||
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -578,7 +574,7 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\
|
||||
--kJBbU58X1xeWNHgBtTbMk80M5qnV4N--\n\
|
||||
";
|
||||
|
||||
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -659,7 +655,7 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\
|
||||
--outer--\n\
|
||||
";
|
||||
|
||||
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -706,7 +702,7 @@ Additional-Message-IDs: <foo@example.com> <foo@example.net>\n\
|
||||
--kJBbU58X1xeWNHgBtTbMk80M5qnV4N--\n\
|
||||
";
|
||||
|
||||
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -753,7 +749,7 @@ MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg==
|
||||
------=_Part_25_46172632.1581201680436--
|
||||
"#;
|
||||
|
||||
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -797,7 +793,7 @@ MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg==
|
||||
------=_Part_25_46172632.1581201680436--
|
||||
"#;
|
||||
|
||||
let message = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap();
|
||||
let message = MimeMessage::from_bytes(&t, &raw[..]).await.unwrap();
|
||||
|
||||
assert_eq!(message.parts.len(), 1);
|
||||
assert_eq!(message.parts[0].typ, Viewtype::File);
|
||||
@@ -839,7 +835,7 @@ iVBORw0KGgoAAAANSUhEUgAAACAAAAAeCAAAAABoYUP1AAAAAXNSR0IArs4c6QAAAo1JREFUKJFdkdFu
|
||||
----11019878869865180--
|
||||
"#;
|
||||
|
||||
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(message.get_subject(), Some("example".to_string()));
|
||||
@@ -903,7 +899,7 @@ iVBORw0KGgoAAAANSUhEUgAAACAAAAAeCAAAAABoYUP1AAAAAXNSR0IArs4c6QAAAo1JREFUKJFdkdFu
|
||||
|
||||
--------------779C1631600DF3DB8C02E53A--"#;
|
||||
|
||||
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(message.get_subject(), Some("Test subject".to_string()));
|
||||
@@ -966,7 +962,7 @@ iVBORw0KGgoAAAANSUhEUgAAACAAAAAeCAAAAABoYUP1AAAAAXNSR0IArs4c6QAAAo1JREFUKJFdkdFu
|
||||
------=_NextPart_000_0003_01D622B3.CA753E60--
|
||||
"#;
|
||||
|
||||
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -1064,7 +1060,7 @@ From: alice <alice@example.org>
|
||||
Reply
|
||||
"##;
|
||||
|
||||
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -1096,7 +1092,7 @@ From: alice <alice@example.org>
|
||||
> Just a quote.
|
||||
"##;
|
||||
|
||||
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -1130,7 +1126,7 @@ On 2020-10-25, Bob wrote:
|
||||
> A quote.
|
||||
"##;
|
||||
|
||||
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(message.get_subject(), Some("Re: top posting".to_string()));
|
||||
@@ -1148,7 +1144,7 @@ On 2020-10-25, Bob wrote:
|
||||
async fn test_attachment_quote() {
|
||||
let context = TestContext::new_alice().await;
|
||||
let raw = include_bytes!("../../test-data/message/quote_attach.eml");
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1166,7 +1162,7 @@ async fn test_attachment_quote() {
|
||||
async fn test_quote_div() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let raw = include_bytes!("../../test-data/message/gmx-quote.eml");
|
||||
let mimeparser = MimeMessage::from_bytes(&t, raw, None).await.unwrap();
|
||||
let mimeparser = MimeMessage::from_bytes(&t, raw).await.unwrap();
|
||||
assert_eq!(mimeparser.parts[0].msg, "YIPPEEEEEE\n\nMulti-line");
|
||||
assert_eq!(mimeparser.parts[0].param.get(Param::Quote).unwrap(), "Now?");
|
||||
}
|
||||
@@ -1176,7 +1172,7 @@ async fn test_allinkl_blockquote() {
|
||||
// all-inkl.com puts quotes into `<blockquote> </blockquote>`.
|
||||
let t = TestContext::new_alice().await;
|
||||
let raw = include_bytes!("../../test-data/message/allinkl-quote.eml");
|
||||
let mimeparser = MimeMessage::from_bytes(&t, raw, None).await.unwrap();
|
||||
let mimeparser = MimeMessage::from_bytes(&t, raw).await.unwrap();
|
||||
assert!(mimeparser.parts[0].msg.starts_with("It's 1.0."));
|
||||
assert_eq!(
|
||||
mimeparser.parts[0].param.get(Param::Quote).unwrap(),
|
||||
@@ -1217,7 +1213,7 @@ async fn test_add_subj_to_multimedia_msg() {
|
||||
async fn test_mime_modified_plain() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let raw = include_bytes!("../../test-data/message/text_plain_unspecified.eml");
|
||||
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw, None).await.unwrap();
|
||||
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
assert!(!mimeparser.is_mime_modified);
|
||||
assert_eq!(
|
||||
mimeparser.parts[0].msg,
|
||||
@@ -1229,7 +1225,7 @@ async fn test_mime_modified_plain() {
|
||||
async fn test_mime_modified_alt_plain_html() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let raw = include_bytes!("../../test-data/message/text_alt_plain_html.eml");
|
||||
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw, None).await.unwrap();
|
||||
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
assert!(mimeparser.is_mime_modified);
|
||||
assert_eq!(
|
||||
mimeparser.parts[0].msg,
|
||||
@@ -1241,7 +1237,7 @@ async fn test_mime_modified_alt_plain_html() {
|
||||
async fn test_mime_modified_alt_plain() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let raw = include_bytes!("../../test-data/message/text_alt_plain.eml");
|
||||
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw, None).await.unwrap();
|
||||
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
assert!(!mimeparser.is_mime_modified);
|
||||
assert_eq!(
|
||||
mimeparser.parts[0].msg,
|
||||
@@ -1256,7 +1252,7 @@ async fn test_mime_modified_alt_plain() {
|
||||
async fn test_mime_modified_alt_html() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let raw = include_bytes!("../../test-data/message/text_alt_html.eml");
|
||||
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw, None).await.unwrap();
|
||||
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
assert!(mimeparser.is_mime_modified);
|
||||
assert_eq!(
|
||||
mimeparser.parts[0].msg,
|
||||
@@ -1268,7 +1264,7 @@ async fn test_mime_modified_alt_html() {
|
||||
async fn test_mime_modified_html() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let raw = include_bytes!("../../test-data/message/text_html.eml");
|
||||
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw, None).await.unwrap();
|
||||
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
assert!(mimeparser.is_mime_modified);
|
||||
assert_eq!(
|
||||
mimeparser.parts[0].msg,
|
||||
@@ -1288,7 +1284,7 @@ async fn test_mime_modified_large_plain() -> Result<()> {
|
||||
assert!(long_txt.len() > DC_DESIRED_TEXT_LEN);
|
||||
|
||||
{
|
||||
let mimemsg = MimeMessage::from_bytes(&t, long_txt.as_ref(), None).await?;
|
||||
let mimemsg = MimeMessage::from_bytes(&t, long_txt.as_ref()).await?;
|
||||
assert!(mimemsg.is_mime_modified);
|
||||
assert!(
|
||||
mimemsg.parts[0].msg.matches("just repeated").count()
|
||||
@@ -1321,7 +1317,7 @@ async fn test_mime_modified_large_plain() -> Result<()> {
|
||||
|
||||
t.set_config(Config::Bot, Some("1")).await?;
|
||||
{
|
||||
let mimemsg = MimeMessage::from_bytes(&t, long_txt.as_ref(), None).await?;
|
||||
let mimemsg = MimeMessage::from_bytes(&t, long_txt.as_ref()).await?;
|
||||
assert!(!mimemsg.is_mime_modified);
|
||||
assert_eq!(
|
||||
format!("{}\n", mimemsg.parts[0].msg),
|
||||
@@ -1368,7 +1364,7 @@ async fn test_x_microsoft_original_message_id() {
|
||||
MIME-Version: 1.0\n\
|
||||
\n\
|
||||
Does it work with outlook now?\n\
|
||||
", None)
|
||||
")
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -1418,7 +1414,7 @@ async fn test_extra_imf_headers() -> Result<()> {
|
||||
"Message-ID:",
|
||||
"Chat-Forty-Two: 42\r\nForty-Two: 42\r\nMessage-ID:",
|
||||
);
|
||||
let msg = MimeMessage::from_bytes(t, payload.as_bytes(), None).await?;
|
||||
let msg = MimeMessage::from_bytes(t, payload.as_bytes()).await?;
|
||||
assert!(msg.headers.contains_key("chat-version"));
|
||||
assert!(!msg.headers.contains_key("chat-forty-two"));
|
||||
assert_ne!(msg.headers.contains_key("forty-two"), std_hp_composing);
|
||||
@@ -1426,6 +1422,40 @@ async fn test_extra_imf_headers() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_intended_recipient_fingerprint() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.alice().await;
|
||||
let t_fp = key::load_self_public_key(t).await?.dc_fingerprint();
|
||||
t.set_config_bool(Config::BccSelf, false).await.unwrap();
|
||||
let members = [tcm.bob().await, tcm.fiona().await];
|
||||
let chat_id = chat::create_group(t, "").await?;
|
||||
|
||||
chat::send_text_msg(t, chat_id, "hi!".to_string()).await?;
|
||||
assert!(t.pop_sent_msg_opt(Duration::ZERO).await.is_none());
|
||||
|
||||
for (i, member) in members.iter().enumerate() {
|
||||
let contact = t.add_or_lookup_contact(member).await;
|
||||
chat::add_contact_to_chat(t, chat_id, contact.id).await?;
|
||||
let sent_msg = t.pop_sent_msg().await;
|
||||
let (fp, recipient_fps) = t.parse_msg(&sent_msg).await.signature.unwrap();
|
||||
assert_eq!(fp, t_fp);
|
||||
// `mimefactory` encrypts to self unconditionally.
|
||||
assert_eq!(recipient_fps.len(), 1 + i + 1);
|
||||
assert!(recipient_fps.contains(&t_fp));
|
||||
assert!(recipient_fps.contains(&contact.fingerprint().unwrap()));
|
||||
}
|
||||
|
||||
t.set_config_bool(Config::BccSelf, true).await.unwrap();
|
||||
chat::send_text_msg(t, chat_id, "hi!".to_string()).await?;
|
||||
let sent_msg = t.pop_sent_msg().await;
|
||||
let (fp, recipient_fps) = t.parse_msg(&sent_msg).await.signature.unwrap();
|
||||
assert_eq!(fp, t_fp);
|
||||
assert_eq!(recipient_fps.len(), 1 + members.len());
|
||||
assert!(recipient_fps.contains(&t_fp));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_long_in_reply_to() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
@@ -1582,7 +1612,7 @@ async fn test_ms_exchange_mdn() -> Result<()> {
|
||||
// 1. Test mimeparser directly
|
||||
let mdn =
|
||||
include_bytes!("../../test-data/message/ms_exchange_report_disposition_notification.eml");
|
||||
let mimeparser = MimeMessage::from_bytes(&t.ctx, mdn, None).await?;
|
||||
let mimeparser = MimeMessage::from_bytes(&t.ctx, mdn).await?;
|
||||
assert_eq!(mimeparser.mdn_reports.len(), 1);
|
||||
assert_eq!(
|
||||
mimeparser.mdn_reports[0].original_message_id.as_deref(),
|
||||
@@ -1608,7 +1638,6 @@ async fn test_receive_eml() -> Result<()> {
|
||||
let mime_message = MimeMessage::from_bytes(
|
||||
&alice,
|
||||
include_bytes!("../../test-data/message/attached-eml.eml"),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1651,7 +1680,6 @@ Content-Disposition: reaction\n\
|
||||
\n\
|
||||
\u{1F44D}"
|
||||
.as_bytes(),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1673,7 +1701,7 @@ async fn test_jpeg_as_application_octet_stream() -> Result<()> {
|
||||
let context = TestContext::new_alice().await;
|
||||
let raw = include_bytes!("../../test-data/message/jpeg-as-application-octet-stream.eml");
|
||||
|
||||
let msg = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||
let msg = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msg.parts.len(), 1);
|
||||
@@ -1691,7 +1719,7 @@ async fn test_schleuder() -> Result<()> {
|
||||
let context = TestContext::new_alice().await;
|
||||
let raw = include_bytes!("../../test-data/message/schleuder.eml");
|
||||
|
||||
let msg = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||
let msg = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msg.parts.len(), 2);
|
||||
@@ -1711,7 +1739,7 @@ async fn test_tlsrpt() -> Result<()> {
|
||||
let context = TestContext::new_alice().await;
|
||||
let raw = include_bytes!("../../test-data/message/tlsrpt.eml");
|
||||
|
||||
let msg = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||
let msg = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msg.parts.len(), 1);
|
||||
@@ -1744,7 +1772,6 @@ async fn test_time_in_future() -> Result<()> {
|
||||
Content-Type: text/plain; charset=utf-8\n\
|
||||
\n\
|
||||
Hi",
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1806,7 +1833,7 @@ Content-Type: text/plain; charset=utf-8
|
||||
/help
|
||||
"#;
|
||||
|
||||
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(message.get_subject(), Some("Some subject".to_string()));
|
||||
@@ -1847,7 +1874,7 @@ async fn test_take_last_header() {
|
||||
Hello\n\
|
||||
";
|
||||
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -1900,9 +1927,7 @@ It DOES end with a linebreak.\r
|
||||
\r
|
||||
This is the epilogue. It is also to be ignored.";
|
||||
|
||||
let mimeparser = MimeMessage::from_bytes(&context, &raw[..], None)
|
||||
.await
|
||||
.unwrap();
|
||||
let mimeparser = MimeMessage::from_bytes(&context, &raw[..]).await.unwrap();
|
||||
|
||||
assert_eq!(mimeparser.parts.len(), 2);
|
||||
|
||||
@@ -1948,7 +1973,7 @@ Message with a correct Message-ID hidden header
|
||||
--luTiGu6GBoVLCvTkzVtmZmwsmhkNMw--
|
||||
"#;
|
||||
|
||||
let message = MimeMessage::from_bytes(t, &raw[..], None).await.unwrap();
|
||||
let message = MimeMessage::from_bytes(t, &raw[..]).await.unwrap();
|
||||
assert_eq!(message.get_rfc724_mid().unwrap(), "foo@example.org");
|
||||
}
|
||||
|
||||
@@ -2126,9 +2151,7 @@ Third alternative.
|
||||
--boundary--
|
||||
"#;
|
||||
|
||||
let message = MimeMessage::from_bytes(context, &raw[..], None)
|
||||
.await
|
||||
.unwrap();
|
||||
let message = MimeMessage::from_bytes(context, &raw[..]).await.unwrap();
|
||||
assert_eq!(message.parts.len(), 1);
|
||||
assert_eq!(message.parts[0].typ, Viewtype::Text);
|
||||
assert_eq!(message.parts[0].msg, "Third alternative.");
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
//! used for successful connection timestamp of
|
||||
//! retrieving them from in-memory cache is used.
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use anyhow::{Context as _, Result, ensure};
|
||||
use std::collections::HashMap;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::str::FromStr;
|
||||
@@ -506,10 +506,6 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
|
||||
"mail.nubo.coop",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(79, 99, 201, 10))],
|
||||
),
|
||||
(
|
||||
"mehl.cloud",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(95, 217, 223, 172))],
|
||||
),
|
||||
(
|
||||
"mx.freenet.de",
|
||||
vec![
|
||||
@@ -680,6 +676,72 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
|
||||
IpAddr::V4(Ipv4Addr::new(185, 230, 214, 164)),
|
||||
],
|
||||
),
|
||||
// Known public chatmail relays from https://chatmail.at/relays
|
||||
(
|
||||
"mehl.cloud",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(95, 217, 223, 172))],
|
||||
),
|
||||
(
|
||||
"mailchat.pl",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(46, 62, 144, 137))],
|
||||
),
|
||||
(
|
||||
"chatmail.woodpeckersnest.space",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(85, 215, 162, 146))],
|
||||
),
|
||||
(
|
||||
"chatmail.culturanerd.it",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(82, 165, 94, 165))],
|
||||
),
|
||||
(
|
||||
"chatmail.hackea.org",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(82, 165, 11, 85))],
|
||||
),
|
||||
(
|
||||
"chika.aangat.lahat.computer",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(71, 19, 150, 113))],
|
||||
),
|
||||
(
|
||||
"tarpit.fun",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(152, 53, 86, 246))],
|
||||
),
|
||||
(
|
||||
"d.gaufr.es",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(51, 77, 140, 91))],
|
||||
),
|
||||
(
|
||||
"chtml.ca",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(51, 222, 156, 177))],
|
||||
),
|
||||
(
|
||||
"chatmail.au",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(45, 124, 54, 79))],
|
||||
),
|
||||
(
|
||||
"sombras.chat",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(82, 25, 70, 154))],
|
||||
),
|
||||
(
|
||||
"e2ee.wang",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(139, 84, 233, 161))],
|
||||
),
|
||||
(
|
||||
"chat.privittytech.com",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(35, 154, 144, 0))],
|
||||
),
|
||||
("e2ee.im", vec![IpAddr::V4(Ipv4Addr::new(45, 137, 99, 57))]),
|
||||
(
|
||||
"chatmail.email",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(57, 128, 220, 120))],
|
||||
),
|
||||
(
|
||||
"danneskjold.de",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(46, 62, 216, 132))],
|
||||
),
|
||||
(
|
||||
"darkrun.dev",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(72, 11, 149, 146))],
|
||||
),
|
||||
])
|
||||
});
|
||||
|
||||
@@ -788,7 +850,7 @@ pub(crate) async fn lookup_host_with_cache(
|
||||
}
|
||||
};
|
||||
|
||||
if load_cache {
|
||||
let addrs = if load_cache {
|
||||
let mut cache = lookup_cache(context, hostname, port, alpn, now).await?;
|
||||
if let Some(ips) = DNS_PRELOAD.get(hostname) {
|
||||
for ip in ips {
|
||||
@@ -799,10 +861,15 @@ pub(crate) async fn lookup_host_with_cache(
|
||||
}
|
||||
}
|
||||
|
||||
Ok(merge_with_cache(resolved_addrs, cache))
|
||||
merge_with_cache(resolved_addrs, cache)
|
||||
} else {
|
||||
Ok(resolved_addrs)
|
||||
}
|
||||
resolved_addrs
|
||||
};
|
||||
ensure!(
|
||||
!addrs.is_empty(),
|
||||
"Could not find DNS resolutions for {hostname}:{port}. Check server hostname and your network"
|
||||
);
|
||||
Ok(addrs)
|
||||
}
|
||||
|
||||
/// Merges results received from DNS with cached results.
|
||||
|
||||
33
src/param.rs
33
src/param.rs
@@ -148,6 +148,9 @@ pub enum Param {
|
||||
/// For Messages
|
||||
WebrtcAccepted = b'7',
|
||||
|
||||
/// For Messages
|
||||
WebrtcHasVideoInitially = b'z',
|
||||
|
||||
/// For Messages: space-separated list of messaged IDs of forwarded copies.
|
||||
///
|
||||
/// This is used when a [crate::message::Message] is in the
|
||||
@@ -251,6 +254,13 @@ pub enum Param {
|
||||
|
||||
/// For info messages: Contact ID in added or removed to a group.
|
||||
ContactAddedRemoved = b'5',
|
||||
|
||||
/// For (pre-)Message: ViewType of the Post-Message,
|
||||
/// because pre message is always `Viewtype::Text`.
|
||||
PostMessageViewtype = b'8',
|
||||
|
||||
/// For (pre-)Message: File byte size of Post-Message attachment
|
||||
PostMessageFileBytes = b'9',
|
||||
}
|
||||
|
||||
/// An object for handling key=value parameter lists.
|
||||
@@ -441,6 +451,15 @@ impl Params {
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Merge in parameters from other Params struct,
|
||||
/// overwriting the keys that are in both
|
||||
/// with the values from the new Params struct.
|
||||
pub fn merge_in_params(&mut self, new_params: Self) -> &mut Self {
|
||||
let mut new_params = new_params;
|
||||
self.inner.append(&mut new_params.inner);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -503,4 +522,18 @@ mod tests {
|
||||
assert_eq!(p.get(Param::Height), Some("14"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge() -> Result<()> {
|
||||
let mut p = Params::from_str("w=12\na=5\nh=14")?;
|
||||
let p2 = Params::from_str("L=1\nh=17")?;
|
||||
assert_eq!(p.len(), 3);
|
||||
p.merge_in_params(p2);
|
||||
assert_eq!(p.len(), 4);
|
||||
assert_eq!(p.get(Param::Width), Some("12"));
|
||||
assert_eq!(p.get(Param::Height), Some("17"));
|
||||
assert_eq!(p.get(Param::Forwarded), Some("5"));
|
||||
assert_eq!(p.get(Param::IsEdited), Some("1"));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
104
src/pgp.rs
104
src/pgp.rs
@@ -1,6 +1,6 @@
|
||||
//! OpenPGP helper module using [rPGP facilities](https://github.com/rpgp/rpgp).
|
||||
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::io::{BufRead, Cursor};
|
||||
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
@@ -10,14 +10,17 @@ use pgp::armor::BlockType;
|
||||
use pgp::composed::{
|
||||
ArmorOptions, DecryptionOptions, Deserializable, DetachedSignature, KeyType as PgpKeyType,
|
||||
Message, MessageBuilder, SecretKeyParamsBuilder, SignedPublicKey, SignedPublicSubKey,
|
||||
SignedSecretKey, SubkeyParamsBuilder, TheRing,
|
||||
SignedSecretKey, SubkeyParamsBuilder, SubpacketConfig, TheRing,
|
||||
};
|
||||
use pgp::crypto::aead::{AeadAlgorithm, ChunkSize};
|
||||
use pgp::crypto::ecc_curve::ECCCurve;
|
||||
use pgp::crypto::hash::HashAlgorithm;
|
||||
use pgp::crypto::sym::SymmetricKeyAlgorithm;
|
||||
use pgp::packet::{SignatureConfig, SignatureType, Subpacket, SubpacketData};
|
||||
use pgp::types::{CompressionAlgorithm, KeyDetails, Password, PublicKeyTrait, StringToKey};
|
||||
use pgp::types::{
|
||||
CompressionAlgorithm, KeyDetails, KeyVersion, Password, PublicKeyTrait, SecretKeyTrait as _,
|
||||
StringToKey,
|
||||
};
|
||||
use rand_old::{Rng as _, thread_rng};
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
@@ -31,9 +34,6 @@ pub(crate) const HEADER_SETUPCODE: &str = "passphrase-begin";
|
||||
/// Preferred symmetric encryption algorithm.
|
||||
const SYMMETRIC_KEY_ALGORITHM: SymmetricKeyAlgorithm = SymmetricKeyAlgorithm::AES128;
|
||||
|
||||
/// Preferred cryptographic hash.
|
||||
const HASH_ALGORITHM: HashAlgorithm = HashAlgorithm::Sha256;
|
||||
|
||||
/// Split data from PGP Armored Data as defined in <https://tools.ietf.org/html/rfc4880#section-6.2>.
|
||||
///
|
||||
/// Returns (type, headers, base64 encoded body).
|
||||
@@ -191,6 +191,36 @@ pub async fn pk_encrypt(
|
||||
let pkeys = public_keys_for_encryption
|
||||
.iter()
|
||||
.filter_map(select_pk_for_encryption);
|
||||
let subpkts = {
|
||||
let mut hashed = Vec::with_capacity(1 + public_keys_for_encryption.len() + 1);
|
||||
hashed.push(Subpacket::critical(SubpacketData::SignatureCreationTime(
|
||||
chrono::Utc::now().trunc_subsecs(0),
|
||||
))?);
|
||||
// Test "elena" uses old Delta Chat.
|
||||
let skip = private_key_for_signing.dc_fingerprint().hex()
|
||||
== "B86586B6DEF437D674BFAFC02A6B2EBC633B9E82";
|
||||
for key in &public_keys_for_encryption {
|
||||
if skip {
|
||||
break;
|
||||
}
|
||||
let data = SubpacketData::IntendedRecipientFingerprint(key.fingerprint());
|
||||
let subpkt = match private_key_for_signing.version() < KeyVersion::V6 {
|
||||
true => Subpacket::regular(data)?,
|
||||
false => Subpacket::critical(data)?,
|
||||
};
|
||||
hashed.push(subpkt);
|
||||
}
|
||||
hashed.push(Subpacket::regular(SubpacketData::IssuerFingerprint(
|
||||
private_key_for_signing.fingerprint(),
|
||||
))?);
|
||||
let mut unhashed = vec![];
|
||||
if private_key_for_signing.version() <= KeyVersion::V4 {
|
||||
unhashed.push(Subpacket::regular(SubpacketData::Issuer(
|
||||
private_key_for_signing.key_id(),
|
||||
))?);
|
||||
}
|
||||
SubpacketConfig::UserDefined { hashed, unhashed }
|
||||
};
|
||||
|
||||
let msg = MessageBuilder::from_bytes("", plain);
|
||||
let encoded_msg = match seipd_version {
|
||||
@@ -205,7 +235,13 @@ pub async fn pk_encrypt(
|
||||
}
|
||||
}
|
||||
|
||||
msg.sign(&*private_key_for_signing, Password::empty(), HASH_ALGORITHM);
|
||||
let hash_algorithm = private_key_for_signing.hash_alg();
|
||||
msg.sign_with_subpackets(
|
||||
&*private_key_for_signing,
|
||||
Password::empty(),
|
||||
hash_algorithm,
|
||||
subpkts,
|
||||
);
|
||||
if compress {
|
||||
msg.compression(CompressionAlgorithm::ZLIB);
|
||||
}
|
||||
@@ -228,7 +264,13 @@ pub async fn pk_encrypt(
|
||||
}
|
||||
}
|
||||
|
||||
msg.sign(&*private_key_for_signing, Password::empty(), HASH_ALGORITHM);
|
||||
let hash_algorithm = private_key_for_signing.hash_alg();
|
||||
msg.sign_with_subpackets(
|
||||
&*private_key_for_signing,
|
||||
Password::empty(),
|
||||
hash_algorithm,
|
||||
subpkts,
|
||||
);
|
||||
if compress {
|
||||
msg.compression(CompressionAlgorithm::ZLIB);
|
||||
}
|
||||
@@ -263,9 +305,14 @@ pub fn pk_calc_signature(
|
||||
chrono::Utc::now().trunc_subsecs(0),
|
||||
))?,
|
||||
];
|
||||
config.unhashed_subpackets = vec![Subpacket::regular(SubpacketData::Issuer(
|
||||
private_key_for_signing.key_id(),
|
||||
))?];
|
||||
config.unhashed_subpackets = vec![];
|
||||
if private_key_for_signing.version() <= KeyVersion::V4 {
|
||||
config
|
||||
.unhashed_subpackets
|
||||
.push(Subpacket::regular(SubpacketData::Issuer(
|
||||
private_key_for_signing.key_id(),
|
||||
))?);
|
||||
}
|
||||
|
||||
let signature = config.sign(
|
||||
&private_key_for_signing.primary_key,
|
||||
@@ -369,19 +416,28 @@ fn check_symmetric_encryption(msg: &Message<'_>) -> std::result::Result<(), &'st
|
||||
|
||||
/// Returns fingerprints
|
||||
/// of all keys from the `public_keys_for_validation` keyring that
|
||||
/// have valid signatures there.
|
||||
/// have valid signatures in `msg` and corresponding intended recipient fingerprints
|
||||
/// (<https://www.rfc-editor.org/rfc/rfc9580.html#name-intended-recipient-fingerpr>) if any.
|
||||
///
|
||||
/// If the message is wrongly signed, HashSet will be empty.
|
||||
/// If the message is wrongly signed, returns an empty map.
|
||||
pub fn valid_signature_fingerprints(
|
||||
msg: &pgp::composed::Message,
|
||||
public_keys_for_validation: &[SignedPublicKey],
|
||||
) -> HashSet<Fingerprint> {
|
||||
let mut ret_signature_fingerprints: HashSet<Fingerprint> = Default::default();
|
||||
) -> HashMap<Fingerprint, Vec<Fingerprint>> {
|
||||
let mut ret_signature_fingerprints = HashMap::new();
|
||||
if msg.is_signed() {
|
||||
for pkey in public_keys_for_validation {
|
||||
if msg.verify(&pkey.primary_key).is_ok() {
|
||||
if let Ok(signature) = msg.verify(&pkey.primary_key) {
|
||||
let fp = pkey.dc_fingerprint();
|
||||
ret_signature_fingerprints.insert(fp);
|
||||
let mut recipient_fps = Vec::new();
|
||||
if let Some(cfg) = signature.config() {
|
||||
for subpkt in &cfg.hashed_subpackets {
|
||||
if let SubpacketData::IntendedRecipientFingerprint(fp) = &subpkt.data {
|
||||
recipient_fps.push(fp.clone().into());
|
||||
}
|
||||
}
|
||||
}
|
||||
ret_signature_fingerprints.insert(fp, recipient_fps);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -453,7 +509,8 @@ pub async fn symm_encrypt_message(
|
||||
);
|
||||
msg.encrypt_with_password(&mut rng, s2k, &shared_secret)?;
|
||||
|
||||
msg.sign(&*private_key_for_signing, Password::empty(), HASH_ALGORITHM);
|
||||
let hash_algorithm = private_key_for_signing.hash_alg();
|
||||
msg.sign(&*private_key_for_signing, Password::empty(), hash_algorithm);
|
||||
if compress {
|
||||
msg.compression(CompressionAlgorithm::ZLIB);
|
||||
}
|
||||
@@ -495,13 +552,14 @@ mod tests {
|
||||
use pgp::composed::Esk;
|
||||
use pgp::packet::PublicKeyEncryptedSessionKey;
|
||||
|
||||
#[expect(clippy::type_complexity)]
|
||||
fn pk_decrypt_and_validate<'a>(
|
||||
ctext: &'a [u8],
|
||||
private_keys_for_decryption: &'a [SignedSecretKey],
|
||||
public_keys_for_validation: &[SignedPublicKey],
|
||||
) -> Result<(
|
||||
pgp::composed::Message<'static>,
|
||||
HashSet<Fingerprint>,
|
||||
HashMap<Fingerprint, Vec<Fingerprint>>,
|
||||
Vec<u8>,
|
||||
)> {
|
||||
let mut msg = decrypt(ctext.to_vec(), private_keys_for_decryption, &[])?;
|
||||
@@ -609,7 +667,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decrypt_singed() {
|
||||
async fn test_decrypt_signed() {
|
||||
// Check decrypting as Alice
|
||||
let decrypt_keyring = vec![KEYS.alice_secret.clone()];
|
||||
let sig_check_keyring = vec![KEYS.alice_public.clone()];
|
||||
@@ -621,6 +679,9 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(content, CLEARTEXT);
|
||||
assert_eq!(valid_signatures.len(), 1);
|
||||
for recipient_fps in valid_signatures.values() {
|
||||
assert_eq!(recipient_fps.len(), 2);
|
||||
}
|
||||
|
||||
// Check decrypting as Bob
|
||||
let decrypt_keyring = vec![KEYS.bob_secret.clone()];
|
||||
@@ -633,6 +694,9 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(content, CLEARTEXT);
|
||||
assert_eq!(valid_signatures.len(), 1);
|
||||
for recipient_fps in valid_signatures.values() {
|
||||
assert_eq!(recipient_fps.len(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
@@ -145,6 +145,7 @@ pub struct Provider {
|
||||
|
||||
/// Provider options with good defaults.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub struct ProviderOptions {
|
||||
/// True if provider is known to use use proper,
|
||||
/// not self-signed certificates.
|
||||
@@ -152,9 +153,6 @@ pub struct ProviderOptions {
|
||||
|
||||
/// Maximum number of recipients the provider allows to send a single email to.
|
||||
pub max_smtp_rcpt_to: Option<u16>,
|
||||
|
||||
/// Move messages to the Trash folder instead of marking them "\Deleted".
|
||||
pub delete_to_trash: bool,
|
||||
}
|
||||
|
||||
impl ProviderOptions {
|
||||
@@ -162,7 +160,6 @@ impl ProviderOptions {
|
||||
Self {
|
||||
strict_tls: true,
|
||||
max_smtp_rcpt_to: None,
|
||||
delete_to_trash: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,16 +505,10 @@ static P_FIVE_CHAT: Provider = Provider {
|
||||
overview_page: "https://providers.delta.chat/five-chat",
|
||||
server: &[],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: Some(&[
|
||||
ConfigDefault {
|
||||
key: Config::BccSelf,
|
||||
value: "1",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::MvboxMove,
|
||||
value: "0",
|
||||
},
|
||||
]),
|
||||
config_defaults: Some(&[ConfigDefault {
|
||||
key: Config::BccSelf,
|
||||
value: "1",
|
||||
}]),
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
@@ -564,7 +558,7 @@ static P_FREENET_DE: Provider = Provider {
|
||||
static P_GMAIL: Provider = Provider {
|
||||
id: "gmail",
|
||||
status: Status::Preparation,
|
||||
before_login_hint: "For Gmail accounts, you need to have \"2-Step Verification\" enabled and create an app-password.",
|
||||
before_login_hint: "For Gmail accounts, you need to have \"2-Step Verification\" enabled and create an app-password. Gmail limits how many messages you can send per day.",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/gmail",
|
||||
server: &[
|
||||
@@ -583,10 +577,7 @@ static P_GMAIL: Provider = Provider {
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
opt: ProviderOptions {
|
||||
delete_to_trash: true,
|
||||
..ProviderOptions::new()
|
||||
},
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: None,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
@@ -1080,10 +1071,6 @@ static P_NAUTA_CU: Provider = Provider {
|
||||
key: Config::DeleteServerAfter,
|
||||
value: "1",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::MvboxMove,
|
||||
value: "0",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::MediaQuality,
|
||||
value: "1",
|
||||
@@ -1172,10 +1159,7 @@ static P_NINE_TESTRUN_ORG: Provider = Provider {
|
||||
},
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: Some(&[ConfigDefault {
|
||||
key: Config::MvboxMove,
|
||||
value: "0",
|
||||
}]),
|
||||
config_defaults: None,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
@@ -1616,16 +1600,10 @@ static P_TESTRUN: Provider = Provider {
|
||||
},
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: Some(&[
|
||||
ConfigDefault {
|
||||
key: Config::BccSelf,
|
||||
value: "1",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::MvboxMove,
|
||||
value: "0",
|
||||
},
|
||||
]),
|
||||
config_defaults: Some(&[ConfigDefault {
|
||||
key: Config::BccSelf,
|
||||
value: "1",
|
||||
}]),
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
@@ -1966,10 +1944,7 @@ static P_YGGMAIL: Provider = Provider {
|
||||
},
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: Some(&[ConfigDefault {
|
||||
key: Config::MvboxMove,
|
||||
value: "0",
|
||||
}]),
|
||||
config_defaults: None,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
@@ -2647,4 +2622,4 @@ pub(crate) static PROVIDER_IDS: LazyLock<HashMap<&'static str, &'static Provider
|
||||
});
|
||||
|
||||
pub static _PROVIDER_UPDATED: LazyLock<chrono::NaiveDate> =
|
||||
LazyLock::new(|| chrono::NaiveDate::from_ymd_opt(2025, 9, 10).unwrap());
|
||||
LazyLock::new(|| chrono::NaiveDate::from_ymd_opt(2026, 1, 28).unwrap());
|
||||
|
||||
@@ -834,6 +834,8 @@ pub(crate) async fn login_param_from_account_qr(
|
||||
}
|
||||
|
||||
/// Sets configuration values from a QR code.
|
||||
/// "DCACCOUNT:" and "DCLOGIN:" QR codes configure `context`, but I/O mustn't be started for such QR
|
||||
/// codes, consider using [`Context::add_transport_from_qr`] which also restarts I/O.
|
||||
pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
|
||||
match check_qr(context, qr).await? {
|
||||
Qr::Account { .. } => {
|
||||
@@ -904,7 +906,7 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
|
||||
.await?;
|
||||
token::save(context, token::Namespace::Auth, None, &authcode, timestamp).await?;
|
||||
context.sync_qr_code_tokens(None).await?;
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
}
|
||||
Qr::ReviveVerifyGroup {
|
||||
invitenumber,
|
||||
@@ -936,7 +938,7 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
|
||||
)
|
||||
.await?;
|
||||
context.sync_qr_code_tokens(Some(&grpid)).await?;
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
}
|
||||
Qr::Login { address, options } => {
|
||||
let mut param = login_param_from_login_qr(&address, options)?;
|
||||
|
||||
@@ -59,14 +59,12 @@ pub enum LoginOptions {
|
||||
/// scheme: `dclogin://user@host/?p=password&v=1[&options]`
|
||||
/// read more about the scheme at <https://github.com/deltachat/interface/blob/master/uri-schemes.md#DCLOGIN>
|
||||
pub(super) fn decode_login(qr: &str) -> Result<Qr> {
|
||||
let url = url::Url::parse(qr).with_context(|| format!("Malformed url: {qr:?}"))?;
|
||||
let qr = qr.replacen("://", ":", 1);
|
||||
|
||||
let url_without_scheme = qr
|
||||
let url = url::Url::parse(&qr).with_context(|| format!("Malformed url: {qr:?}"))?;
|
||||
let payload = qr
|
||||
.get(DCLOGIN_SCHEME.len()..)
|
||||
.context("invalid DCLOGIN payload E1")?;
|
||||
let payload = url_without_scheme
|
||||
.strip_prefix("//")
|
||||
.unwrap_or(url_without_scheme);
|
||||
|
||||
let addr = payload
|
||||
.split(['?', '/'])
|
||||
@@ -365,4 +363,32 @@ mod test {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_dclogin_ipv4() -> anyhow::Result<()> {
|
||||
let result = decode_login("dclogin://test@[127.0.0.1]?p=1234&v=1")?;
|
||||
if let Qr::Login { address, options } = result {
|
||||
assert_eq!(address, "test@[127.0.0.1]".to_owned());
|
||||
assert_eq!(options, login_options_just_pw!("1234".to_owned()));
|
||||
} else {
|
||||
unreachable!("wrong type");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_dclogin_ipv6() -> anyhow::Result<()> {
|
||||
let result =
|
||||
decode_login("dclogin://test@[2001:0db8:85a3:0000:0000:8a2e:0370:7334]?p=1234&v=1")?;
|
||||
if let Qr::Login { address, options } = result {
|
||||
assert_eq!(
|
||||
address,
|
||||
"test@[2001:0db8:85a3:0000:0000:8a2e:0370:7334]".to_owned()
|
||||
);
|
||||
assert_eq!(options, login_options_just_pw!("1234".to_owned()));
|
||||
} else {
|
||||
unreachable!("wrong type");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
56
src/quota.rs
56
src/quota.rs
@@ -107,10 +107,10 @@ pub fn needs_quota_warning(curr_percentage: u64, warned_at_percentage: u64) -> b
|
||||
impl Context {
|
||||
/// Returns whether the quota value needs an update. If so, `update_recent_quota()` should be
|
||||
/// called.
|
||||
pub(crate) async fn quota_needs_update(&self, ratelimit_secs: u64) -> bool {
|
||||
pub(crate) async fn quota_needs_update(&self, transport_id: u32, ratelimit_secs: u64) -> bool {
|
||||
let quota = self.quota.read().await;
|
||||
quota
|
||||
.as_ref()
|
||||
.get(&transport_id)
|
||||
.filter(|quota| time_elapsed("a.modified) < Duration::from_secs(ratelimit_secs))
|
||||
.is_none()
|
||||
}
|
||||
@@ -155,10 +155,13 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
*self.quota.write().await = Some(QuotaInfo {
|
||||
recent: quota,
|
||||
modified: tools::Time::now(),
|
||||
});
|
||||
self.quota.write().await.insert(
|
||||
session.transport_id(),
|
||||
QuotaInfo {
|
||||
recent: quota,
|
||||
modified: tools::Time::now(),
|
||||
},
|
||||
);
|
||||
|
||||
self.emit_event(EventType::ConnectivityChanged);
|
||||
Ok(())
|
||||
@@ -203,27 +206,42 @@ mod tests {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.unconfigured().await;
|
||||
const TIMEOUT: u64 = 60;
|
||||
assert!(t.quota_needs_update(TIMEOUT).await);
|
||||
assert!(t.quota_needs_update(0, TIMEOUT).await);
|
||||
|
||||
*t.quota.write().await = Some(QuotaInfo {
|
||||
recent: Ok(Default::default()),
|
||||
modified: tools::Time::now() - Duration::from_secs(TIMEOUT + 1),
|
||||
});
|
||||
assert!(t.quota_needs_update(TIMEOUT).await);
|
||||
*t.quota.write().await = {
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert(
|
||||
0,
|
||||
QuotaInfo {
|
||||
recent: Ok(Default::default()),
|
||||
modified: tools::Time::now() - Duration::from_secs(TIMEOUT + 1),
|
||||
},
|
||||
);
|
||||
map
|
||||
};
|
||||
assert!(t.quota_needs_update(0, TIMEOUT).await);
|
||||
|
||||
*t.quota.write().await = Some(QuotaInfo {
|
||||
recent: Ok(Default::default()),
|
||||
modified: tools::Time::now(),
|
||||
});
|
||||
assert!(!t.quota_needs_update(TIMEOUT).await);
|
||||
*t.quota.write().await = {
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert(
|
||||
0,
|
||||
QuotaInfo {
|
||||
recent: Ok(Default::default()),
|
||||
modified: tools::Time::now(),
|
||||
},
|
||||
);
|
||||
map
|
||||
};
|
||||
assert!(!t.quota_needs_update(0, TIMEOUT).await);
|
||||
|
||||
t.evtracker.clear_events();
|
||||
t.set_primary_self_addr("new@addr").await?;
|
||||
assert!(t.quota.read().await.is_none());
|
||||
assert!(t.quota.read().await.is_empty());
|
||||
t.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ConnectivityChanged))
|
||||
.await;
|
||||
assert!(t.quota_needs_update(TIMEOUT).await);
|
||||
assert!(t.quota_needs_update(0, TIMEOUT).await);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -392,9 +392,8 @@ mod tests {
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::config::Config;
|
||||
use crate::contact::{Contact, Origin};
|
||||
use crate::download::DownloadState;
|
||||
use crate::message::{MessageState, Viewtype, delete_msgs};
|
||||
use crate::receive_imf::{receive_imf, receive_imf_from_inbox};
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::sql::housekeeping;
|
||||
use crate::test_utils::E2EE_INFO_MSGS;
|
||||
use crate::test_utils::TestContext;
|
||||
@@ -748,13 +747,23 @@ Content-Disposition: reaction\n\
|
||||
alice_reaction_msg.id.get_state(&alice).await?,
|
||||
MessageState::InSeen
|
||||
);
|
||||
// Reactions don't request MDNs.
|
||||
// Reactions don't request MDNs, but an MDN to self is sent.
|
||||
assert_eq!(
|
||||
alice
|
||||
.sql
|
||||
.count("SELECT COUNT(*) FROM smtp_mdns", ())
|
||||
.await?,
|
||||
0
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
alice
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM smtp_mdns WHERE from_id=?",
|
||||
(ContactId::SELF,)
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
|
||||
// Alice reacts to own message.
|
||||
@@ -924,73 +933,6 @@ Content-Disposition: reaction\n\
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_partial_download_and_reaction() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
alice
|
||||
.create_chat_with_contact("Bob", "bob@example.net")
|
||||
.await;
|
||||
|
||||
let msg_header = "From: Bob <bob@example.net>\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";
|
||||
let msg_full = format!("{msg_header}\n\n100k text...");
|
||||
|
||||
// Alice downloads message from Bob partially.
|
||||
let alice_received_message = receive_imf_from_inbox(
|
||||
&alice,
|
||||
"first@example.org",
|
||||
msg_header.as_bytes(),
|
||||
false,
|
||||
Some(100000),
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
let alice_msg_id = *alice_received_message.msg_ids.first().unwrap();
|
||||
|
||||
// Bob downloads own message on the other device.
|
||||
let bob_received_message = receive_imf(&bob, msg_full.as_bytes(), false)
|
||||
.await?
|
||||
.unwrap();
|
||||
let bob_msg_id = *bob_received_message.msg_ids.first().unwrap();
|
||||
|
||||
// Bob reacts to own message.
|
||||
send_reaction(&bob, bob_msg_id, "👍").await.unwrap();
|
||||
let bob_reaction_msg = bob.pop_sent_msg().await;
|
||||
|
||||
// Alice receives a reaction.
|
||||
alice.recv_msg_hidden(&bob_reaction_msg).await;
|
||||
|
||||
let reactions = get_msg_reactions(&alice, alice_msg_id).await?;
|
||||
assert_eq!(reactions.to_string(), "👍1");
|
||||
let msg = Message::load_from_db(&alice, alice_msg_id).await?;
|
||||
assert_eq!(msg.download_state(), DownloadState::Available);
|
||||
|
||||
// Alice downloads full message.
|
||||
receive_imf_from_inbox(
|
||||
&alice,
|
||||
"first@example.org",
|
||||
msg_full.as_bytes(),
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Check that reaction is still on the message after full download.
|
||||
let msg = Message::load_from_db(&alice, alice_msg_id).await?;
|
||||
assert_eq!(msg.download_state(), DownloadState::Done);
|
||||
let reactions = get_msg_reactions(&alice, alice_msg_id).await?;
|
||||
assert_eq!(reactions.to_string(), "👍1");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_reaction_multidevice() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,13 +4,13 @@ use tokio::fs;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{
|
||||
ChatItem, ChatVisibility, add_contact_to_chat, add_to_chat_contacts_table, create_group,
|
||||
get_chat_contacts, get_chat_msgs, is_contact_in_chat, remove_contact_from_chat, send_text_msg,
|
||||
CantSendReason, ChatItem, ChatVisibility, add_contact_to_chat, add_to_chat_contacts_table,
|
||||
create_group, get_chat_contacts, get_chat_msgs, is_contact_in_chat, remove_contact_from_chat,
|
||||
send_text_msg,
|
||||
};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::DC_GCL_FOR_FORWARDING;
|
||||
use crate::contact;
|
||||
use crate::download::MIN_DOWNLOAD_LIMIT;
|
||||
use crate::imap::prefetch_should_download;
|
||||
use crate::imex::{ImexMode, imex};
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
@@ -19,8 +19,6 @@ use crate::test_utils::{
|
||||
};
|
||||
use crate::tools::{SystemTime, time};
|
||||
|
||||
use rand::distr::SampleString;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_outgoing() -> Result<()> {
|
||||
let context = TestContext::new_alice().await;
|
||||
@@ -28,7 +26,7 @@ async fn test_outgoing() -> Result<()> {
|
||||
From: alice@example.org\n\
|
||||
\n\
|
||||
hello";
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None).await?;
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]).await?;
|
||||
assert_eq!(mimeparser.incoming, false);
|
||||
Ok(())
|
||||
}
|
||||
@@ -43,7 +41,7 @@ async fn test_bad_from() {
|
||||
References: <Gr.HcxyMARjyJy.9-uvzWPTLtV@nauta.cu>\n\
|
||||
\n\
|
||||
hello\x00";
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None).await;
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]).await;
|
||||
assert!(mimeparser.is_err());
|
||||
}
|
||||
|
||||
@@ -2842,7 +2840,7 @@ References: <second@example.net> <nonexistent@example.net> <first@example.net>
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||
|
||||
Message with references."#;
|
||||
let mime_parser = MimeMessage::from_bytes(&t, &mime[..], None).await?;
|
||||
let mime_parser = MimeMessage::from_bytes(&t, &mime[..]).await?;
|
||||
|
||||
let parent = get_parent_message(&t, &mime_parser).await?.unwrap();
|
||||
assert_eq!(parent.id, first.id);
|
||||
@@ -3286,7 +3284,8 @@ async fn test_blocked_contact_creates_group() -> Result<()> {
|
||||
|
||||
let sent = bob.send_text(group_id, "Heyho, I'm a spammer!").await;
|
||||
let rcvd = alice.recv_msg(&sent).await;
|
||||
// Alice blocked Bob, so she shouldn't get the message
|
||||
// Alice blocked Bob, so she shouldn't be notified.
|
||||
assert_eq!(rcvd.state, MessageState::InSeen);
|
||||
assert_eq!(rcvd.chat_blocked, Blocked::Yes);
|
||||
|
||||
// Fiona didn't block Bob, though, so she gets the message
|
||||
@@ -3852,6 +3851,61 @@ async fn test_sync_member_list_on_rejoin() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_group_contacts_goto_bottom() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
|
||||
let bob_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let fiona_id = alice.add_or_lookup_contact_id(fiona).await;
|
||||
|
||||
let alice_chat_id = create_group(alice, "Testing contact list").await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, bob_id).await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, fiona_id).await?;
|
||||
|
||||
send_text_msg(alice, alice_chat_id, "hello".to_string()).await?;
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
let bob_chat_id = bob.get_last_msg().await.chat_id;
|
||||
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 3);
|
||||
assert_eq!(Contact::get_all(bob, 0, None).await?.len(), 0);
|
||||
bob_chat_id.accept(bob).await?;
|
||||
let contacts = Contact::get_all(bob, 0, None).await?;
|
||||
assert_eq!(contacts.len(), 2);
|
||||
let bob_fiona_id = bob.add_or_lookup_contact_id(fiona).await;
|
||||
assert_eq!(contacts[1], bob_fiona_id);
|
||||
|
||||
ChatId::create_for_contact(bob, bob_fiona_id).await?;
|
||||
let contacts = Contact::get_all(bob, 0, None).await?;
|
||||
assert_eq!(contacts.len(), 2);
|
||||
assert_eq!(contacts[0], bob_fiona_id);
|
||||
|
||||
send_text_msg(
|
||||
bob,
|
||||
bob_chat_id,
|
||||
"Hi Alice, stay down in my contact list".to_string(),
|
||||
)
|
||||
.await?;
|
||||
bob.pop_sent_msg().await;
|
||||
let contacts = Contact::get_all(bob, 0, None).await?;
|
||||
assert_eq!(contacts[0], bob_fiona_id);
|
||||
|
||||
remove_contact_from_chat(bob, bob_chat_id, bob_fiona_id).await?;
|
||||
bob.pop_sent_msg().await;
|
||||
let contacts = Contact::get_all(bob, 0, None).await?;
|
||||
// Fiona is still the 0th contact. This makes sense, maybe Bob is going to remove Alice from the
|
||||
// chat too, so no need to make Alice a more "important" contact yet.
|
||||
assert_eq!(contacts[0], bob_fiona_id);
|
||||
|
||||
send_text_msg(bob, bob_chat_id, "Alice, jump up!".to_string()).await?;
|
||||
bob.pop_sent_msg().await;
|
||||
let contacts = Contact::get_all(bob, 0, None).await?;
|
||||
let bob_alice_id = bob.add_or_lookup_contact_id(alice).await;
|
||||
assert_eq!(contacts[0], bob_alice_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test for the bug when remote group membership changes from outdated messages overrode local
|
||||
/// ones. Especially that was a problem when a message is sent offline so that it doesn't
|
||||
/// incorporate recent group membership changes.
|
||||
@@ -4385,37 +4439,6 @@ async fn test_adhoc_grp_name_no_prefix() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_download_later() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
alice.set_config(Config::DownloadLimit, Some("1")).await?;
|
||||
assert_eq!(alice.download_limit().await?, Some(MIN_DOWNLOAD_LIMIT));
|
||||
|
||||
let bob = tcm.bob().await;
|
||||
let bob_chat = bob.create_chat(&alice).await;
|
||||
|
||||
// Generate a random string so OpenPGP does not compress it.
|
||||
let text =
|
||||
rand::distr::Alphanumeric.sample_string(&mut rand::rng(), MIN_DOWNLOAD_LIMIT as usize);
|
||||
|
||||
let sent_msg = bob.send_text(bob_chat.id, &text).await;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.download_state, DownloadState::Available);
|
||||
assert_eq!(msg.state, MessageState::InFresh);
|
||||
|
||||
let hi_msg = tcm.send_recv(&bob, &alice, "hi").await;
|
||||
|
||||
alice.set_config(Config::DownloadLimit, None).await?;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.download_state, DownloadState::Done);
|
||||
assert_eq!(msg.state, MessageState::InFresh);
|
||||
assert_eq!(alice.get_last_msg_in(msg.chat_id).await.id, hi_msg.id);
|
||||
assert!(msg.timestamp_sort <= hi_msg.timestamp_sort);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Malice can pretend they have the same address as Alice and sends a message encrypted to Alice's
|
||||
/// key but signed with another one. Alice must detect that this message is wrongly signed and not
|
||||
/// treat it as Autocrypt-encrypted.
|
||||
@@ -4451,158 +4474,50 @@ async fn test_outgoing_msg_forgery() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_group_with_big_msg() -> Result<()> {
|
||||
async fn test_pre_msg_group_consistency() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let ba_contact = bob.add_or_lookup_contact_id(&alice).await;
|
||||
let ab_chat_id = alice.create_chat(&bob).await.id;
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
let bob_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let alice_chat_id = create_group(alice, "foos").await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, bob_id).await?;
|
||||
|
||||
let file_bytes = include_bytes!("../../test-data/image/screenshot.png");
|
||||
|
||||
let bob_grp_id = create_group(&bob, "Group").await?;
|
||||
add_contact_to_chat(&bob, bob_grp_id, ba_contact).await?;
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file_from_bytes(&bob, "a.jpg", file_bytes, None)?;
|
||||
let sent_msg = bob.send_msg(bob_grp_id, &mut msg).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
alice.set_config(Config::DownloadLimit, Some("1")).await?;
|
||||
assert_eq!(alice.download_limit().await?, Some(MIN_DOWNLOAD_LIMIT));
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.download_state, DownloadState::Available);
|
||||
let alice_chat = Chat::load_from_db(&alice, msg.chat_id).await?;
|
||||
// Incomplete message is assigned to 1:1 chat.
|
||||
assert_eq!(alice_chat.typ, Chattype::Single);
|
||||
|
||||
alice.set_config(Config::DownloadLimit, None).await?;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.download_state, DownloadState::Done);
|
||||
assert_eq!(msg.state, MessageState::InFresh);
|
||||
assert_eq!(msg.viewtype, Viewtype::Image);
|
||||
assert_ne!(msg.chat_id, alice_chat.id);
|
||||
let alice_grp = Chat::load_from_db(&alice, msg.chat_id).await?;
|
||||
assert_eq!(alice_grp.typ, Chattype::Group);
|
||||
assert_eq!(alice_grp.name, "Group");
|
||||
assert_eq!(
|
||||
chat::get_chat_contacts(&alice, alice_grp.id).await?.len(),
|
||||
2
|
||||
);
|
||||
|
||||
// Now Bob can send encrypted messages to Alice.
|
||||
|
||||
let bob_grp_id = create_group(&bob, "Group1").await?;
|
||||
add_contact_to_chat(&bob, bob_grp_id, ba_contact).await?;
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file_from_bytes(&bob, "a.jpg", file_bytes, None)?;
|
||||
let sent_msg = bob.send_msg(bob_grp_id, &mut msg).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
alice.set_config(Config::DownloadLimit, Some("1")).await?;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.download_state, DownloadState::Available);
|
||||
// Until fully downloaded, an encrypted message must sit in the 1:1 chat.
|
||||
assert_eq!(msg.chat_id, ab_chat_id);
|
||||
|
||||
alice.set_config(Config::DownloadLimit, None).await?;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.download_state, DownloadState::Done);
|
||||
assert_eq!(msg.state, MessageState::InFresh);
|
||||
assert_eq!(msg.viewtype, Viewtype::Image);
|
||||
assert_ne!(msg.chat_id, ab_chat_id);
|
||||
let alice_grp = Chat::load_from_db(&alice, msg.chat_id).await?;
|
||||
assert_eq!(alice_grp.typ, Chattype::Group);
|
||||
assert_eq!(alice_grp.name, "Group1");
|
||||
assert_eq!(
|
||||
chat::get_chat_contacts(&alice, alice_grp.id).await?.len(),
|
||||
2
|
||||
);
|
||||
|
||||
// The big message must go away from the 1:1 chat.
|
||||
let msgs = chat::get_chat_msgs(&alice, ab_chat_id).await?;
|
||||
assert_eq!(msgs.len(), E2EE_INFO_MSGS);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_partial_group_consistency() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let fiona = tcm.fiona().await;
|
||||
let bob_id = alice.add_or_lookup_contact_id(&bob).await;
|
||||
let alice_chat_id = create_group(&alice, "foos").await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
|
||||
|
||||
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
|
||||
send_text_msg(alice, alice_chat_id, "populate".to_string()).await?;
|
||||
let add = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&add).await;
|
||||
let bob_chat_id = bob.get_last_msg().await.chat_id;
|
||||
let contacts = get_chat_contacts(&bob, bob_chat_id).await?;
|
||||
bob_chat_id.accept(bob).await?;
|
||||
let contacts = get_chat_contacts(bob, bob_chat_id).await?;
|
||||
assert_eq!(contacts.len(), 2);
|
||||
|
||||
// Bob receives partial message.
|
||||
let msg_id = receive_imf_from_inbox(
|
||||
&bob,
|
||||
"first@example.org",
|
||||
b"From: Alice <alice@example.org>\n\
|
||||
To: <bob@example.net>, <charlie@example.com>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: subject\n\
|
||||
Message-ID: <first@example.org>\n\
|
||||
Date: Sun, 14 Nov 2021 00:10:00 +0000\
|
||||
Content-Type: text/plain
|
||||
Chat-Group-Member-Added: charlie@example.com",
|
||||
false,
|
||||
Some(100000),
|
||||
)
|
||||
.await?
|
||||
.context("no received message")?;
|
||||
|
||||
let msg = Message::load_from_db(&bob, msg_id.msg_ids[0]).await?;
|
||||
|
||||
// Partial download does not change the member list.
|
||||
assert_eq!(msg.download_state, DownloadState::Available);
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?, contacts);
|
||||
|
||||
// Alice sends normal message to bob, adding fiona.
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice,
|
||||
alice_chat_id,
|
||||
alice.add_or_lookup_contact_id(&fiona).await,
|
||||
alice.add_or_lookup_contact_id(fiona).await,
|
||||
)
|
||||
.await?;
|
||||
// This message is lost.
|
||||
alice.pop_sent_msg().await;
|
||||
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
|
||||
let contacts = get_chat_contacts(&bob, bob_chat_id).await?;
|
||||
// Pre-message adds the new member.
|
||||
let file_bytes = include_bytes!("../../test-data/image/screenshot.gif");
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file_from_bytes(alice, "a.jpg", file_bytes, None)?;
|
||||
let full_msg = alice.send_msg(alice_chat_id, &mut msg).await;
|
||||
let pre_msg = alice.pop_sent_msg().await;
|
||||
let msg = bob.recv_msg(&pre_msg).await;
|
||||
assert_eq!(msg.download_state, DownloadState::Available);
|
||||
let contacts = get_chat_contacts(bob, bob_chat_id).await?;
|
||||
assert_eq!(contacts.len(), 3);
|
||||
|
||||
// Bob fully receives the partial message.
|
||||
let msg_id = receive_imf_from_inbox(
|
||||
&bob,
|
||||
"first@example.org",
|
||||
b"From: Alice <alice@example.org>\n\
|
||||
To: Bob <bob@example.net>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: subject\n\
|
||||
Message-ID: <first@example.org>\n\
|
||||
Date: Sun, 14 Nov 2021 00:10:00 +0000\
|
||||
Content-Type: text/plain
|
||||
Chat-Group-Member-Added: charlie@example.com",
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
.context("no received message")?;
|
||||
|
||||
let msg = Message::load_from_db(&bob, msg_id.msg_ids[0]).await?;
|
||||
|
||||
// After full download, the old message should not change group state.
|
||||
assert_eq!(msg.download_state, DownloadState::Done);
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?, contacts);
|
||||
remove_contact_from_chat(bob, bob_chat_id, bob.add_or_lookup_contact_id(fiona).await).await?;
|
||||
bob.pop_sent_msg().await;
|
||||
|
||||
// Full message doesn't readd the removed member.
|
||||
bob.recv_msg_trash(&full_msg).await;
|
||||
let contacts = get_chat_contacts(bob, bob_chat_id).await?;
|
||||
assert_eq!(contacts.len(), 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4844,48 +4759,6 @@ async fn test_references() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_prefer_references_to_downloaded_msgs() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
bob.set_config(Config::DownloadLimit, Some("1")).await?;
|
||||
let fiona = &tcm.fiona().await;
|
||||
let alice_bob_id = tcm.send_recv(bob, alice, "hi").await.from_id;
|
||||
let alice_fiona_id = tcm.send_recv(fiona, alice, "hi").await.from_id;
|
||||
let alice_chat_id = create_group(alice, "Group").await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_bob_id).await?;
|
||||
// W/o fiona the test doesn't work -- the last message is assigned to the 1:1 chat due to
|
||||
// `is_probably_private_reply()`.
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_fiona_id).await?;
|
||||
let sent = alice.send_text(alice_chat_id, "Hi").await;
|
||||
let received = bob.recv_msg(&sent).await;
|
||||
assert_eq!(received.download_state, DownloadState::Done);
|
||||
let bob_chat_id = received.chat_id;
|
||||
|
||||
let file_bytes = include_bytes!("../../test-data/image/screenshot.gif");
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file_from_bytes(alice, "file", file_bytes, None)?;
|
||||
let mut sent = alice.send_msg(alice_chat_id, &mut msg).await;
|
||||
sent.payload = sent
|
||||
.payload
|
||||
.replace("References:", "X-Microsoft-Original-References:")
|
||||
.replace("In-Reply-To:", "X-Microsoft-Original-In-Reply-To:");
|
||||
let received = bob.recv_msg(&sent).await;
|
||||
assert_eq!(received.download_state, DownloadState::Available);
|
||||
assert_ne!(received.chat_id, bob_chat_id);
|
||||
assert_eq!(received.chat_id, bob.get_chat(alice).await.id);
|
||||
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file_from_bytes(alice, "file", file_bytes, None)?;
|
||||
let sent = alice.send_msg(alice_chat_id, &mut msg).await;
|
||||
let received = bob.recv_msg(&sent).await;
|
||||
assert_eq!(received.download_state, DownloadState::Available);
|
||||
assert_eq!(received.chat_id, bob_chat_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_list_from() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
@@ -4900,7 +4773,7 @@ async fn test_list_from() -> Result<()> {
|
||||
"clubinfo@donotreply.oeamtc.at"
|
||||
);
|
||||
let info = msg.id.get_info(t).await?;
|
||||
assert!(info.contains(" by ~ÖAMTC (clubinfo@donotreply.oeamtc.at)"));
|
||||
assert!(info.contains(" by ~ÖAMTC"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -5235,6 +5108,100 @@ async fn test_dont_verify_by_verified_by_unknown() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recv_outgoing_msg_before_securejoin() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let bob = &tcm.bob().await;
|
||||
let a0 = &tcm.elena().await;
|
||||
let a1 = &tcm.elena().await;
|
||||
|
||||
tcm.execute_securejoin(bob, a0).await;
|
||||
let chat_id_a0_bob = a0.create_chat_id(bob).await;
|
||||
let sent_msg = a0.send_text(chat_id_a0_bob, "Hi").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let msg_a1 = a1.recv_msg(&sent_msg).await;
|
||||
assert!(msg_a1.get_showpadlock());
|
||||
let chat_a1 = Chat::load_from_db(a1, msg_a1.chat_id).await?;
|
||||
assert_eq!(chat_a1.typ, Chattype::Group);
|
||||
assert!(!chat_a1.is_encrypted(a1).await?);
|
||||
assert_eq!(
|
||||
chat::get_chat_contacts(a1, chat_a1.id).await?,
|
||||
[a1.add_or_lookup_address_contact_id(bob).await]
|
||||
);
|
||||
assert_eq!(
|
||||
chat_a1.why_cant_send(a1).await?,
|
||||
Some(CantSendReason::NotAMember)
|
||||
);
|
||||
|
||||
let sent_msg = a0.send_text(chat_id_a0_bob, "Hi again").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let msg_a1 = a1.recv_msg(&sent_msg).await;
|
||||
assert!(msg_a1.get_showpadlock());
|
||||
assert_eq!(msg_a1.chat_id, chat_a1.id);
|
||||
let chat_a1 = Chat::load_from_db(a1, msg_a1.chat_id).await?;
|
||||
assert_eq!(
|
||||
chat_a1.why_cant_send(a1).await?,
|
||||
Some(CantSendReason::NotAMember)
|
||||
);
|
||||
|
||||
let msg_a1 = tcm.send_recv(bob, a1, "Hi back").await;
|
||||
assert!(msg_a1.get_showpadlock());
|
||||
let chat_a1 = Chat::load_from_db(a1, msg_a1.chat_id).await?;
|
||||
assert_eq!(chat_a1.typ, Chattype::Single);
|
||||
assert!(chat_a1.is_encrypted(a1).await?);
|
||||
// Weird, but fine, anyway the bigger problem is the conversation split into two chats.
|
||||
assert_eq!(
|
||||
chat_a1.why_cant_send(a1).await?,
|
||||
Some(CantSendReason::ContactRequest)
|
||||
);
|
||||
|
||||
let a0 = &tcm.alice().await;
|
||||
let a1 = &tcm.alice().await;
|
||||
tcm.execute_securejoin(bob, a0).await;
|
||||
let chat_id_a0_bob = a0.create_chat_id(bob).await;
|
||||
let sent_msg = a0.send_text(chat_id_a0_bob, "Hi").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let msg_a1 = a1.recv_msg(&sent_msg).await;
|
||||
assert!(msg_a1.get_showpadlock());
|
||||
let chat_a1 = Chat::load_from_db(a1, msg_a1.chat_id).await?;
|
||||
assert_eq!(chat_a1.typ, Chattype::Single);
|
||||
assert!(chat_a1.is_encrypted(a1).await?);
|
||||
assert_eq!(
|
||||
chat::get_chat_contacts(a1, chat_a1.id).await?,
|
||||
[a1.add_or_lookup_contact_id(bob).await]
|
||||
);
|
||||
assert!(chat_a1.can_send(a1).await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recv_outgoing_msg_before_having_key_and_after() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let a0 = &tcm.elena().await;
|
||||
let a1 = &tcm.elena().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
tcm.execute_securejoin(bob, a0).await;
|
||||
let chat_id_a0_bob = a0.create_chat_id(bob).await;
|
||||
let sent_msg = a0.send_text(chat_id_a0_bob, "Hi").await;
|
||||
let msg_a1 = a1.recv_msg(&sent_msg).await;
|
||||
assert!(msg_a1.get_showpadlock());
|
||||
let chat_a1 = Chat::load_from_db(a1, msg_a1.chat_id).await?;
|
||||
assert_eq!(chat_a1.typ, Chattype::Group);
|
||||
assert!(!chat_a1.is_encrypted(a1).await?);
|
||||
|
||||
// Device a1 somehow learns Bob's key and creates the corresponding chat. However, this doesn't
|
||||
// help because we only look up key contacts by address in a particular chat and the new chat
|
||||
// isn't referenced by the received message. This is fixed by sending and receiving Intended
|
||||
// Recipient Fingerprint subpackets which elena doesn't send.
|
||||
a1.create_chat_id(bob).await;
|
||||
let sent_msg = a0.send_text(chat_id_a0_bob, "Hi again").await;
|
||||
let msg_a1 = a1.recv_msg(&sent_msg).await;
|
||||
assert!(msg_a1.get_showpadlock());
|
||||
assert_eq!(msg_a1.chat_id, chat_a1.id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sanitize_filename_in_received() -> Result<()> {
|
||||
let alice = &TestContext::new_alice().await;
|
||||
@@ -5363,41 +5330,6 @@ async fn test_outgoing_plaintext_two_member_group() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that large messages are assigned
|
||||
/// to non-key-contacts if the type is not `multipart/encrypted`.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_partial_download_key_contact_lookup() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
// Create two chats with Alice, both with key-contact and email address contact.
|
||||
let encrypted_chat = bob.create_chat(alice).await;
|
||||
let unencrypted_chat = bob.create_email_chat(alice).await;
|
||||
|
||||
let seen = false;
|
||||
let is_partial_download = Some(9999);
|
||||
let received = receive_imf_from_inbox(
|
||||
bob,
|
||||
"3333@example.org",
|
||||
b"From: alice@example.org\n\
|
||||
To: bob@example.net\n\
|
||||
Message-ID: <3333@example.org>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
seen,
|
||||
is_partial_download,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
|
||||
assert_ne!(received.chat_id, encrypted_chat.id);
|
||||
assert_eq!(received.chat_id, unencrypted_chat.id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that outgoing unencrypted message
|
||||
/// is assigned to a chat with email-contact.
|
||||
///
|
||||
@@ -5529,16 +5461,14 @@ async fn test_encrypted_adhoc_group_message() -> Result<()> {
|
||||
assert_eq!(chat.is_encrypted(bob).await?, false);
|
||||
|
||||
let contact_ids = get_chat_contacts(bob, chat.id).await?;
|
||||
assert_eq!(contact_ids.len(), 3);
|
||||
assert!(chat.is_self_in_chat(bob).await?);
|
||||
assert_eq!(contact_ids.len(), 2);
|
||||
assert!(!chat.is_self_in_chat(bob).await?);
|
||||
|
||||
// Since the group is unencrypted, all contacts have
|
||||
// to be address-contacts.
|
||||
for contact_id in contact_ids {
|
||||
let contact = Contact::get_by_id(bob, contact_id).await?;
|
||||
if contact_id != ContactId::SELF {
|
||||
assert_eq!(contact.is_key_contact(), false);
|
||||
}
|
||||
assert_eq!(contact.is_key_contact(), false);
|
||||
}
|
||||
|
||||
// `from_id` of the message corresponds to key-contact of Alice
|
||||
|
||||
159
src/scheduler.rs
159
src/scheduler.rs
@@ -11,16 +11,15 @@ use tokio_util::sync::CancellationToken;
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
pub(crate) use self::connectivity::ConnectivityStore;
|
||||
use crate::config::{self, Config};
|
||||
use crate::config::Config;
|
||||
use crate::contact::{ContactId, RecentlySeenLoop};
|
||||
use crate::context::Context;
|
||||
use crate::download::{DownloadState, download_msg};
|
||||
use crate::download::{download_known_post_messages_without_pre_message, download_msgs};
|
||||
use crate::ephemeral::{self, delete_expired_imap_messages};
|
||||
use crate::events::EventType;
|
||||
use crate::imap::{FolderMeaning, Imap, session::Session};
|
||||
use crate::location;
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::message::MsgId;
|
||||
use crate::smtp::{Smtp, send_smtp_messages};
|
||||
use crate::sql;
|
||||
use crate::stats::maybe_send_stats;
|
||||
@@ -257,14 +256,6 @@ impl SchedulerState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Interrupt optional boxes (mvbox currently) loops.
|
||||
pub(crate) async fn interrupt_oboxes(&self) {
|
||||
let inner = self.inner.read().await;
|
||||
if let InnerSchedulerState::Started(ref scheduler) = *inner {
|
||||
scheduler.interrupt_oboxes();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn interrupt_smtp(&self) {
|
||||
let inner = self.inner.read().await;
|
||||
if let InnerSchedulerState::Started(ref scheduler) = *inner {
|
||||
@@ -325,8 +316,8 @@ impl Drop for IoPausedGuard {
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SchedBox {
|
||||
/// Hostname of used chatmail/email relay
|
||||
host: String,
|
||||
/// Address at the used chatmail/email relay
|
||||
addr: String,
|
||||
meaning: FolderMeaning,
|
||||
conn_state: ImapConnectionState,
|
||||
|
||||
@@ -351,38 +342,6 @@ pub(crate) struct Scheduler {
|
||||
recently_seen_loop: RecentlySeenLoop,
|
||||
}
|
||||
|
||||
async fn download_msgs(context: &Context, session: &mut Session) -> Result<()> {
|
||||
let msg_ids = context
|
||||
.sql
|
||||
.query_map_vec("SELECT msg_id FROM download", (), |row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
Ok(msg_id)
|
||||
})
|
||||
.await?;
|
||||
|
||||
for msg_id in msg_ids {
|
||||
if let Err(err) = download_msg(context, msg_id, session).await {
|
||||
warn!(context, "Failed to download message {msg_id}: {:#}.", err);
|
||||
|
||||
// Update download state to failure
|
||||
// so it can be retried.
|
||||
//
|
||||
// On success update_download_state() is not needed
|
||||
// as receive_imf() already
|
||||
// set the state and emitted the event.
|
||||
msg_id
|
||||
.update_download_state(context, DownloadState::Failure)
|
||||
.await?;
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM download WHERE msg_id=?", (msg_id,))
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn inbox_loop(
|
||||
ctx: Context,
|
||||
started: oneshot::Sender<()>,
|
||||
@@ -481,7 +440,7 @@ async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session)
|
||||
}
|
||||
|
||||
// Update quota no more than once a minute.
|
||||
if ctx.quota_needs_update(60).await
|
||||
if ctx.quota_needs_update(session.transport_id(), 60).await
|
||||
&& let Err(err) = ctx.update_recent_quota(&mut session).await
|
||||
{
|
||||
warn!(ctx, "Failed to update quota: {:#}.", err);
|
||||
@@ -510,37 +469,11 @@ async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session)
|
||||
};
|
||||
|
||||
maybe_send_stats(ctx).await.log_err(ctx).ok();
|
||||
match ctx.get_config_bool(Config::FetchedExistingMsgs).await {
|
||||
Ok(fetched_existing_msgs) => {
|
||||
if !fetched_existing_msgs {
|
||||
// Consider it done even if we fail.
|
||||
//
|
||||
// This operation is not critical enough to retry,
|
||||
// especially if the error is persistent.
|
||||
if let Err(err) = ctx
|
||||
.set_config_internal(Config::FetchedExistingMsgs, config::from_bool(true))
|
||||
.await
|
||||
{
|
||||
warn!(ctx, "Can't set Config::FetchedExistingMsgs: {:#}", err);
|
||||
}
|
||||
|
||||
if let Err(err) = imap.fetch_existing_msgs(ctx, &mut session).await {
|
||||
warn!(ctx, "Failed to fetch existing messages: {:#}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(ctx, "Can't get Config::FetchedExistingMsgs: {:#}", err);
|
||||
}
|
||||
}
|
||||
|
||||
download_msgs(ctx, &mut session)
|
||||
.await
|
||||
.context("Failed to download messages")?;
|
||||
session
|
||||
.fetch_metadata(ctx)
|
||||
.update_metadata(ctx)
|
||||
.await
|
||||
.context("Failed to fetch metadata")?;
|
||||
.context("update_metadata")?;
|
||||
session
|
||||
.register_token(ctx)
|
||||
.await
|
||||
@@ -572,56 +505,30 @@ async fn fetch_idle(
|
||||
};
|
||||
|
||||
if folder_config == Config::ConfiguredInboxFolder {
|
||||
let mvbox;
|
||||
let syncbox = match ctx.should_move_sync_msgs().await? {
|
||||
false => &watch_folder,
|
||||
true => {
|
||||
mvbox = ctx.get_config(Config::ConfiguredMvboxFolder).await?;
|
||||
mvbox.as_deref().unwrap_or(&watch_folder)
|
||||
}
|
||||
};
|
||||
if ctx
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.unwrap_or_default()
|
||||
== connection.addr
|
||||
{
|
||||
session
|
||||
.send_sync_msgs(ctx, syncbox)
|
||||
.await
|
||||
.context("fetch_idle: send_sync_msgs")
|
||||
.log_err(ctx)
|
||||
.ok();
|
||||
}
|
||||
|
||||
session
|
||||
.store_seen_flags_on_imap(ctx)
|
||||
.await
|
||||
.context("store_seen_flags_on_imap")?;
|
||||
}
|
||||
|
||||
if !ctx.should_delete_to_trash().await?
|
||||
|| ctx
|
||||
.get_config(Config::ConfiguredTrashFolder)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
// Fetch the watched folder.
|
||||
connection
|
||||
.fetch_move_delete(ctx, &mut session, &watch_folder, folder_meaning)
|
||||
.await
|
||||
.context("fetch_move_delete")?;
|
||||
// Fetch the watched folder.
|
||||
connection
|
||||
.fetch_move_delete(ctx, &mut session, &watch_folder, folder_meaning)
|
||||
.await
|
||||
.context("fetch_move_delete")?;
|
||||
|
||||
// Mark expired messages for deletion. Marked messages will be deleted from the server
|
||||
// on the next iteration of `fetch_move_delete`. `delete_expired_imap_messages` is not
|
||||
// called right before `fetch_move_delete` because it is not well optimized and would
|
||||
// otherwise slow down message fetching.
|
||||
delete_expired_imap_messages(ctx)
|
||||
.await
|
||||
.context("delete_expired_imap_messages")?;
|
||||
} else if folder_config == Config::ConfiguredInboxFolder {
|
||||
session.last_full_folder_scan.lock().await.take();
|
||||
}
|
||||
// Mark expired messages for deletion. Marked messages will be deleted from the server
|
||||
// on the next iteration of `fetch_move_delete`. `delete_expired_imap_messages` is not
|
||||
// called right before `fetch_move_delete` because it is not well optimized and would
|
||||
// otherwise slow down message fetching.
|
||||
delete_expired_imap_messages(ctx)
|
||||
.await
|
||||
.context("delete_expired_imap_messages")?;
|
||||
|
||||
download_known_post_messages_without_pre_message(ctx, &mut session).await?;
|
||||
download_msgs(ctx, &mut session)
|
||||
.await
|
||||
.context("download_msgs")?;
|
||||
|
||||
// Scan additional folders only after finishing fetching the watched folder.
|
||||
//
|
||||
@@ -704,6 +611,7 @@ async fn fetch_idle(
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
/// Simplified IMAP loop to watch non-inbox folders.
|
||||
async fn simple_imap_loop(
|
||||
ctx: Context,
|
||||
started: oneshot::Sender<()>,
|
||||
@@ -883,14 +791,9 @@ impl Scheduler {
|
||||
let ctx = ctx.clone();
|
||||
task::spawn(inbox_loop(ctx, inbox_start_send, inbox_handlers))
|
||||
};
|
||||
let host = configured_login_param
|
||||
.addr
|
||||
.split("@")
|
||||
.last()
|
||||
.context("address has no host")?
|
||||
.to_owned();
|
||||
let addr = configured_login_param.addr.clone();
|
||||
let inbox = SchedBox {
|
||||
host: host.clone(),
|
||||
addr: addr.clone(),
|
||||
meaning: FolderMeaning::Inbox,
|
||||
conn_state,
|
||||
handle,
|
||||
@@ -906,7 +809,7 @@ impl Scheduler {
|
||||
let meaning = FolderMeaning::Mvbox;
|
||||
let handle = task::spawn(simple_imap_loop(ctx, start_send, handlers, meaning));
|
||||
oboxes.push(SchedBox {
|
||||
host,
|
||||
addr,
|
||||
meaning,
|
||||
conn_state,
|
||||
handle,
|
||||
@@ -982,12 +885,6 @@ impl Scheduler {
|
||||
}
|
||||
}
|
||||
|
||||
fn interrupt_oboxes(&self) {
|
||||
for b in &self.oboxes {
|
||||
b.conn_state.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
fn interrupt_smtp(&self) {
|
||||
self.smtp.interrupt();
|
||||
}
|
||||
|
||||
@@ -343,9 +343,18 @@ impl Context {
|
||||
.green {
|
||||
background-color: #34c759;
|
||||
}
|
||||
.grey {
|
||||
background-color: #808080;
|
||||
}
|
||||
.yellow {
|
||||
background-color: #fdc625;
|
||||
}
|
||||
.transport {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.quota-list {
|
||||
padding-left: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>"#
|
||||
@@ -375,7 +384,7 @@ impl Context {
|
||||
.boxes()
|
||||
.map(|b| {
|
||||
(
|
||||
b.host.clone(),
|
||||
b.addr.clone(),
|
||||
b.meaning,
|
||||
b.conn_state.state.connectivity.clone(),
|
||||
)
|
||||
@@ -396,83 +405,85 @@ impl Context {
|
||||
// =============================================================================================
|
||||
// Add e.g.
|
||||
// Incoming messages
|
||||
// - "Inbox": Connected
|
||||
// - [X] nine.testrun.org: Connected
|
||||
// 1.34 GiB of 2 GiB used
|
||||
// [======67%===== ]
|
||||
// =============================================================================================
|
||||
|
||||
let watched_folders = get_watched_folder_configs(self).await?;
|
||||
let incoming_messages = stock_str::incoming_messages(self).await;
|
||||
ret += &format!("<h3>{incoming_messages}</h3><ul>");
|
||||
for (host, folder, state) in &folders_states {
|
||||
let mut folder_added = false;
|
||||
|
||||
if let Some(config) = folder.to_config().filter(|c| watched_folders.contains(c)) {
|
||||
let f = self.get_config(config).await.log_err(self).ok().flatten();
|
||||
|
||||
if let Some(foldername) = f {
|
||||
let detailed = &state.get_detailed();
|
||||
ret += "<li>";
|
||||
ret += &*detailed.to_icon();
|
||||
ret += " <b>";
|
||||
if folder == &FolderMeaning::Inbox {
|
||||
ret += &*escaper::encode_minimal(host);
|
||||
} else {
|
||||
ret += &*escaper::encode_minimal(&foldername);
|
||||
}
|
||||
ret += ":</b> ";
|
||||
ret += &*escaper::encode_minimal(&detailed.to_string_imap(self).await);
|
||||
ret += "</li>";
|
||||
|
||||
folder_added = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !folder_added && folder == &FolderMeaning::Inbox {
|
||||
let detailed = &state.get_detailed();
|
||||
if let DetailedConnectivity::Error(_) = detailed {
|
||||
// On the inbox thread, we also do some other things like scan_folders and run jobs
|
||||
// so, maybe, the inbox is not watched, but something else went wrong
|
||||
ret += "<li>";
|
||||
ret += &*detailed.to_icon();
|
||||
ret += " ";
|
||||
ret += &*escaper::encode_minimal(&detailed.to_string_imap(self).await);
|
||||
ret += "</li>";
|
||||
}
|
||||
}
|
||||
}
|
||||
ret += "</ul>";
|
||||
|
||||
// =============================================================================================
|
||||
// Add e.g.
|
||||
// Outgoing messages
|
||||
// Your last message was sent successfully
|
||||
// =============================================================================================
|
||||
|
||||
let outgoing_messages = stock_str::outgoing_messages(self).await;
|
||||
ret += &format!("<h3>{outgoing_messages}</h3><ul><li>");
|
||||
let detailed = smtp.get_detailed();
|
||||
ret += &*detailed.to_icon();
|
||||
ret += " ";
|
||||
ret += &*escaper::encode_minimal(&detailed.to_string_smtp(self).await);
|
||||
ret += "</li></ul>";
|
||||
|
||||
// =============================================================================================
|
||||
// Add e.g.
|
||||
// Storage on testrun.org
|
||||
// 1.34 GiB of 2 GiB used
|
||||
// [======67%===== ]
|
||||
// =============================================================================================
|
||||
|
||||
let domain =
|
||||
&deltachat_contact_tools::EmailAddress::new(&self.get_primary_self_addr().await?)?
|
||||
.domain;
|
||||
let storage_on_domain =
|
||||
escaper::encode_minimal(&stock_str::storage_on_domain(self, domain).await);
|
||||
ret += &format!("<h3>{storage_on_domain}</h3><ul>");
|
||||
let transports = self
|
||||
.sql
|
||||
.query_map_vec("SELECT id, addr FROM transports", (), |row| {
|
||||
let transport_id: u32 = row.get(0)?;
|
||||
let addr: String = row.get(1)?;
|
||||
Ok((transport_id, addr))
|
||||
})
|
||||
.await?;
|
||||
let quota = self.quota.read().await;
|
||||
if let Some(quota) = &*quota {
|
||||
for (transport_id, transport_addr) in transports {
|
||||
let domain = &deltachat_contact_tools::EmailAddress::new(&transport_addr)
|
||||
.map_or(transport_addr.clone(), |email| email.domain);
|
||||
let domain_escaped = escaper::encode_minimal(domain);
|
||||
|
||||
ret += "<li class=\"transport\">";
|
||||
let folders = folders_states
|
||||
.iter()
|
||||
.filter(|(folder_addr, ..)| *folder_addr == transport_addr);
|
||||
for (_addr, folder, state) in folders {
|
||||
let mut folder_added = false;
|
||||
|
||||
if let Some(config) = folder.to_config().filter(|c| watched_folders.contains(c)) {
|
||||
let f = self.get_config(config).await.log_err(self).ok().flatten();
|
||||
|
||||
if let Some(foldername) = f {
|
||||
let detailed = &state.get_detailed();
|
||||
ret += &*detailed.to_icon();
|
||||
ret += " <b>";
|
||||
if folder == &FolderMeaning::Inbox {
|
||||
ret += &*domain_escaped;
|
||||
} else {
|
||||
ret += &*escaper::encode_minimal(&foldername);
|
||||
}
|
||||
ret += ":</b> ";
|
||||
ret += &*escaper::encode_minimal(&detailed.to_string_imap(self).await);
|
||||
ret += "<br />";
|
||||
|
||||
folder_added = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !folder_added && folder == &FolderMeaning::Inbox {
|
||||
let detailed = &state.get_detailed();
|
||||
if let DetailedConnectivity::Error(_) = detailed {
|
||||
// On the inbox thread, we also do some other things like scan_folders and run jobs
|
||||
// so, maybe, the inbox is not watched, but something else went wrong
|
||||
|
||||
ret += &*detailed.to_icon();
|
||||
ret += " ";
|
||||
ret += &*escaper::encode_minimal(&detailed.to_string_imap(self).await);
|
||||
ret += "<br />";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let Some(quota) = quota.get(&transport_id) else {
|
||||
ret += "</li>";
|
||||
continue;
|
||||
};
|
||||
match "a.recent {
|
||||
Err(e) => {
|
||||
ret += &escaper::encode_minimal(&e.to_string());
|
||||
}
|
||||
Ok(quota) => {
|
||||
if !quota.is_empty() {
|
||||
if quota.is_empty() {
|
||||
ret += &format!(
|
||||
"Warning: {domain_escaped} claims to support quota but gives no information"
|
||||
);
|
||||
} else {
|
||||
ret += "<ul class=\"quota-list\">";
|
||||
for (root_name, resources) in quota {
|
||||
use async_imap::types::QuotaResourceName::*;
|
||||
for resource in resources {
|
||||
@@ -529,7 +540,7 @@ impl Context {
|
||||
} else if percent >= QUOTA_WARN_THRESHOLD_PERCENTAGE {
|
||||
"yellow"
|
||||
} else {
|
||||
"green"
|
||||
"grey"
|
||||
};
|
||||
let div_width_percent = min(100, percent);
|
||||
ret += &format!(
|
||||
@@ -539,24 +550,28 @@ impl Context {
|
||||
ret += "</li>";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let domain_escaped = escaper::encode_minimal(domain);
|
||||
ret += &format!(
|
||||
"<li>Warning: {domain_escaped} claims to support quota but gives no information</li>"
|
||||
);
|
||||
ret += "</ul>";
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let error_escaped = escaper::encode_minimal(&e.to_string());
|
||||
ret += &format!("<li>{error_escaped}</li>");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let not_connected = stock_str::not_connected(self).await;
|
||||
ret += &format!("<li>{not_connected}</li>");
|
||||
ret += "</li>";
|
||||
}
|
||||
ret += "</ul>";
|
||||
|
||||
// =============================================================================================
|
||||
// Add e.g.
|
||||
// Outgoing messages
|
||||
// Your last message was sent successfully
|
||||
// =============================================================================================
|
||||
|
||||
let outgoing_messages = stock_str::outgoing_messages(self).await;
|
||||
ret += &format!("<h3>{outgoing_messages}</h3><ul><li>");
|
||||
let detailed = smtp.get_detailed();
|
||||
ret += &*detailed.to_icon();
|
||||
ret += " ";
|
||||
ret += &*escaper::encode_minimal(&detailed.to_string_smtp(self).await);
|
||||
ret += "</li></ul>";
|
||||
|
||||
// =============================================================================================
|
||||
|
||||
ret += "</body></html>\n";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user