mirror of
https://github.com/chatmail/core.git
synced 2026-04-05 23:22:11 +03:00
Compare commits
131 Commits
v2.21.0
...
link2xt/ni
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6162fc1bc5 | ||
|
|
1b572361f5 | ||
|
|
8f99cf810f | ||
|
|
da3d35e3ff | ||
|
|
83529099b4 | ||
|
|
c6ace749e3 | ||
|
|
22ebd6436f | ||
|
|
cdfe436124 | ||
|
|
e8823fcf35 | ||
|
|
0136cfaf6a | ||
|
|
07069c348b | ||
|
|
26f6b85ff9 | ||
|
|
10b6dd1f11 | ||
|
|
cae642b024 | ||
|
|
54a2e94525 | ||
|
|
9d4ad00fc0 | ||
|
|
102b72aadd | ||
|
|
1c4d2dd78e | ||
|
|
cd50c263e8 | ||
|
|
1dbcd7f1f4 | ||
|
|
c6894f56b2 | ||
|
|
e2ae6ae013 | ||
|
|
966ea28f83 | ||
|
|
6611a9fa02 | ||
|
|
dc4ea1865a | ||
|
|
4b1dff601d | ||
|
|
a66808e25a | ||
|
|
7b54954401 | ||
|
|
d39ed9d0f1 | ||
|
|
c499dabbe1 | ||
|
|
e70307af1f | ||
|
|
69a3a31554 | ||
|
|
1cb0a25e16 | ||
|
|
fdea6c8af3 | ||
|
|
2e9fd1c25d | ||
|
|
1b1a5f170e | ||
|
|
1946603be6 | ||
|
|
c43b622c23 | ||
|
|
73bf6983b9 | ||
|
|
aaa0f8e245 | ||
|
|
5a1e0e8824 | ||
|
|
cf5b145ce0 | ||
|
|
dd11a0e29a | ||
|
|
3d86cb5953 | ||
|
|
75eb94e44f | ||
|
|
7fef812b1e | ||
|
|
5f174ceaf2 | ||
|
|
06b038ab5d | ||
|
|
b20da3cb0e | ||
|
|
a3328ea2de | ||
|
|
ee75094bef | ||
|
|
a40fd288fc | ||
|
|
81ba2d20d6 | ||
|
|
f04c881b8c | ||
|
|
ee6b9075aa | ||
|
|
9c2a13b88e | ||
|
|
1db6ea70cc | ||
|
|
da2d9620cd | ||
|
|
d1dcb739f2 | ||
|
|
e34687ba42 | ||
|
|
5034449009 | ||
|
|
997e8216bf | ||
|
|
7f059140be | ||
|
|
c9b3da4a1a | ||
|
|
098084b9a7 | ||
|
|
9bc2aeebb8 | ||
|
|
56370c2f90 | ||
|
|
59959259bf | ||
|
|
08f8f488b1 | ||
|
|
f34311d5c4 | ||
|
|
885a5efa39 | ||
|
|
8b4c718b6b | ||
|
|
2ada3cd613 | ||
|
|
b920552fc3 | ||
|
|
92c31903c6 | ||
|
|
145145f0fb | ||
|
|
05ba206c5a | ||
|
|
9f0d106818 | ||
|
|
21caf87119 | ||
|
|
4abc695790 | ||
|
|
df1a7ca386 | ||
|
|
a06ba35ce1 | ||
|
|
18445c09c2 | ||
|
|
f428033d95 | ||
|
|
0e30dd895f | ||
|
|
c001a9a983 | ||
|
|
5f3948b462 | ||
|
|
45a1d81805 | ||
|
|
19d7799324 | ||
|
|
24e18c1485 | ||
|
|
3eb1a7dfac | ||
|
|
2f2a147efb | ||
|
|
f4938465c3 | ||
|
|
129137b5de | ||
|
|
ec3f765727 | ||
|
|
a743ad9490 | ||
|
|
89315b8ef2 | ||
|
|
e7348a4fd8 | ||
|
|
c68244692d | ||
|
|
3c93f61b4d | ||
|
|
51b9e86d71 | ||
|
|
347938a9f9 | ||
|
|
9897ef2e9b | ||
|
|
2f34a740c7 | ||
|
|
fc81cef113 | ||
|
|
04c2585c27 | ||
|
|
59fac54f7b | ||
|
|
65b61efb31 | ||
|
|
afc74b0829 | ||
|
|
2481a0f48e | ||
|
|
6c24edb40d | ||
|
|
e4178789da | ||
|
|
b417ba86bc | ||
|
|
498a831873 | ||
|
|
c6722d36de | ||
|
|
90f0d5c060 | ||
|
|
90ec2f2518 | ||
|
|
5b66535134 | ||
|
|
eea848f72b | ||
|
|
214a1d3e2d | ||
|
|
e270a502d1 | ||
|
|
b863345600 | ||
|
|
61b49a9339 | ||
|
|
41c80cf3f2 | ||
|
|
6fd3645360 | ||
|
|
b812d0a7f7 | ||
|
|
e8a4c9237d | ||
|
|
5256013615 | ||
|
|
9826c28581 | ||
|
|
9ceceebdc3 | ||
|
|
187d913f84 |
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -7,6 +7,8 @@ updates:
|
||||
commit-message:
|
||||
prefix: "chore(cargo)"
|
||||
open-pull-requests-limit: 50
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
# Keep GitHub Actions up to date.
|
||||
# <https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot>
|
||||
@@ -14,3 +16,5 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@@ -20,10 +20,10 @@ permissions: {}
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
RUST_VERSION: 1.90.0
|
||||
RUST_VERSION: 1.91.0
|
||||
|
||||
# Minimum Supported Rust Version
|
||||
MSRV: 1.85.0
|
||||
MSRV: 1.88.0
|
||||
|
||||
jobs:
|
||||
lint_rust:
|
||||
@@ -168,7 +168,7 @@ jobs:
|
||||
run: cargo build -p deltachat_ffi
|
||||
|
||||
- name: Upload C library
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: ${{ matrix.os }}-libdeltachat.a
|
||||
path: target/debug/libdeltachat.a
|
||||
@@ -193,7 +193,7 @@ jobs:
|
||||
run: cargo build -p deltachat-rpc-server
|
||||
|
||||
- name: Upload deltachat-rpc-server
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: ${{ matrix.os }}-deltachat-rpc-server
|
||||
path: ${{ matrix.os == 'windows-latest' && 'target/debug/deltachat-rpc-server.exe' || 'target/debug/deltachat-rpc-server' }}
|
||||
@@ -252,7 +252,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download libdeltachat.a
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: ${{ matrix.os }}-libdeltachat.a
|
||||
path: target/debug
|
||||
@@ -313,7 +313,7 @@ jobs:
|
||||
run: pip install tox
|
||||
|
||||
- name: Download deltachat-rpc-server
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: ${{ matrix.os }}-deltachat-rpc-server
|
||||
path: target/debug
|
||||
|
||||
64
.github/workflows/deltachat-rpc-server.yml
vendored
64
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -34,13 +34,13 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
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@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}
|
||||
path: result/bin/deltachat-rpc-server.exe
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.arch }}-apple-darwin --features vendored
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-macos
|
||||
path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server
|
||||
@@ -109,13 +109,13 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-android
|
||||
path: result/bin/deltachat-rpc-server
|
||||
@@ -136,70 +136,70 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
|
||||
|
||||
- name: Download Linux aarch64 binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-aarch64-linux
|
||||
path: deltachat-rpc-server-aarch64-linux.d
|
||||
|
||||
- name: Download Linux armv7l binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-armv7l-linux
|
||||
path: deltachat-rpc-server-armv7l-linux.d
|
||||
|
||||
- name: Download Linux armv6l binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-armv6l-linux
|
||||
path: deltachat-rpc-server-armv6l-linux.d
|
||||
|
||||
- name: Download Linux i686 binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-i686-linux
|
||||
path: deltachat-rpc-server-i686-linux.d
|
||||
|
||||
- name: Download Linux x86_64 binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64-linux
|
||||
path: deltachat-rpc-server-x86_64-linux.d
|
||||
|
||||
- name: Download Win32 binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-win32
|
||||
path: deltachat-rpc-server-win32.d
|
||||
|
||||
- name: Download Win64 binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-win64
|
||||
path: deltachat-rpc-server-win64.d
|
||||
|
||||
- name: Download macOS binary for x86_64
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64-macos
|
||||
path: deltachat-rpc-server-x86_64-macos.d
|
||||
|
||||
- name: Download macOS binary for aarch64
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-aarch64-macos
|
||||
path: deltachat-rpc-server-aarch64-macos.d
|
||||
|
||||
- name: Download Android binary for arm64-v8a
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-arm64-v8a-android
|
||||
path: deltachat-rpc-server-arm64-v8a-android.d
|
||||
|
||||
- name: Download Android binary for armeabi-v7a
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-armeabi-v7a-android
|
||||
path: deltachat-rpc-server-armeabi-v7a-android.d
|
||||
@@ -294,67 +294,67 @@ jobs:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Download Linux aarch64 binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-aarch64-linux
|
||||
path: deltachat-rpc-server-aarch64-linux.d
|
||||
|
||||
- name: Download Linux armv7l binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-armv7l-linux
|
||||
path: deltachat-rpc-server-armv7l-linux.d
|
||||
|
||||
- name: Download Linux armv6l binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-armv6l-linux
|
||||
path: deltachat-rpc-server-armv6l-linux.d
|
||||
|
||||
- name: Download Linux i686 binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-i686-linux
|
||||
path: deltachat-rpc-server-i686-linux.d
|
||||
|
||||
- name: Download Linux x86_64 binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64-linux
|
||||
path: deltachat-rpc-server-x86_64-linux.d
|
||||
|
||||
- name: Download Win32 binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-win32
|
||||
path: deltachat-rpc-server-win32.d
|
||||
|
||||
- name: Download Win64 binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-win64
|
||||
path: deltachat-rpc-server-win64.d
|
||||
|
||||
- name: Download macOS binary for x86_64
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64-macos
|
||||
path: deltachat-rpc-server-x86_64-macos.d
|
||||
|
||||
- name: Download macOS binary for aarch64
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-aarch64-macos
|
||||
path: deltachat-rpc-server-aarch64-macos.d
|
||||
|
||||
- name: Download Android binary for arm64-v8a
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-arm64-v8a-android
|
||||
path: deltachat-rpc-server-arm64-v8a-android.d
|
||||
|
||||
- name: Download Android binary for armeabi-v7a
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-armeabi-v7a-android
|
||||
path: deltachat-rpc-server-armeabi-v7a-android.d
|
||||
@@ -384,7 +384,7 @@ jobs:
|
||||
ls -lah
|
||||
|
||||
- name: Upload to artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: deltachat-rpc-server-npm-package
|
||||
path: deltachat-rpc-server/npm-package/*.tgz
|
||||
@@ -401,7 +401,7 @@ jobs:
|
||||
deltachat-rpc-server/npm-package/*.tgz
|
||||
|
||||
# Configure Node.js for publishing.
|
||||
- uses: actions/setup-node@v5
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@v5
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
2
.github/workflows/jsonrpc.yml
vendored
2
.github/workflows/jsonrpc.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 18.x
|
||||
- name: Add Rust cache
|
||||
|
||||
11
.github/workflows/nix.yml
vendored
11
.github/workflows/nix.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # 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@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
build-macos:
|
||||
@@ -95,14 +95,15 @@ jobs:
|
||||
matrix:
|
||||
installable:
|
||||
- deltachat-rpc-server
|
||||
- deltachat-rpc-server-x86_64-darwin
|
||||
|
||||
# Fails to bulid
|
||||
# Fails to build
|
||||
# because of <https://github.com/NixOS/nixpkgs/issues/413910>.
|
||||
# - deltachat-rpc-server-aarch64-darwin
|
||||
# - deltachat-rpc-server-x86_64-darwin
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # 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@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: python-package-distributions
|
||||
path: deltachat-rpc-client/dist/
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Download all the dists
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: python-package-distributions
|
||||
path: dist/
|
||||
|
||||
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@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
|
||||
- name: Build
|
||||
run: nix build .#deltachat-repl-win64
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: repl.exe
|
||||
path: "result/bin/deltachat-repl.exe"
|
||||
|
||||
6
.github/workflows/upload-docs.yml
vendored
6
.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@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # 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@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
|
||||
- name: Build C documentation
|
||||
run: nix build .#docs
|
||||
- name: Upload to c.delta.chat
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '18'
|
||||
- name: npm install
|
||||
|
||||
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@v6
|
||||
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41
|
||||
|
||||
- name: Run zizmor
|
||||
run: uvx zizmor --format sarif . > results.sarif
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -36,6 +36,7 @@ deltachat-ffi/xml
|
||||
coverage/
|
||||
.DS_Store
|
||||
.vscode
|
||||
.zed
|
||||
python/accounts.txt
|
||||
python/all-testaccounts.txt
|
||||
tmp/
|
||||
|
||||
228
CHANGELOG.md
228
CHANGELOG.md
@@ -1,5 +1,227 @@
|
||||
# Changelog
|
||||
|
||||
## [2.27.0] - 2025-11-16
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add APIs to stop background fetch.
|
||||
- [**breaking**]: rename JSON-RPC method accounts_background_fetch() into background_fetch()
|
||||
- rpc-client: Add APIs for background fetch.
|
||||
- rpc-client: Add Account.wait_for_msg().
|
||||
- Deprecate deletion timer string for '1 Minute'.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Implement RFC 9788 (Header Protection for Cryptographically Protected Email) ([#7130](https://github.com/chatmail/core/pull/7130)).
|
||||
- Tweak initial info-message for unencrypted chats ([#7427](https://github.com/chatmail/core/pull/7427)).
|
||||
- Add Contact::get_or_gen_color. Use it in CFFI and JSON-RPC to avoid gray self-color ([#7374](https://github.com/chatmail/core/pull/7374)).
|
||||
- [**breaking**] Withdraw broadcast invites. Add Qr::WithdrawJoinBroadcast and Qr::ReviveJoinBroadcast QR code types. ([#7439](https://github.com/chatmail/core/pull/7439)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Set `get_max_smtp_rcpt_to` for chatmail to the actual limit of 1000 instead of unlimited. ([#7432](https://github.com/chatmail/core/pull/7432)).
|
||||
- Always set bcc_self on backup import/export.
|
||||
- Escape connectivity HTML.
|
||||
- Send webm as file, it is not supported by all UI.
|
||||
|
||||
### Build system
|
||||
|
||||
- nix: Exclude CONTRIBUTING.md from the source files.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Use wait_for_incoming_msg() in more tests.
|
||||
|
||||
### Tests
|
||||
|
||||
- Fix flaky test_send_receive_locations.
|
||||
- Port folder-related CFFI tests to JSON-RPC.
|
||||
- HP-Outer headers are added to messages with standard Header Protection ([#7130](https://github.com/chatmail/core/pull/7130)).
|
||||
- rpc-client: Test_qr_securejoin_broadcast: Wait for incoming message before getting chatlist ([#7442](https://github.com/chatmail/core/pull/7442)).
|
||||
- Add pytest fixture for account manager.
|
||||
- Test background_fetch() and stop_background_fetch().
|
||||
|
||||
## [2.26.0] - 2025-11-11
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] JSON-RPC: `chat_type` now contains a variant of a string enum/union. Affected places: `FullChat.chat_type`, `BasicChat.chat_type`, `ChatListItemFetchResult::ChatListItem.chat_type`, `Event:: SecurejoinInviterProgress.chat_type` and `MessageSearchResult.chat_type` ([#7285](https://github.com/chatmail/core/pull/7285))
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Error toast for "Not creating securejoin QR for old broadcast".
|
||||
|
||||
### Fixes
|
||||
|
||||
- `is_encrypted()` should be true for Saved Messages chat so messages there are editable.
|
||||
- Do not return an error from `receive_imf` if we fail to add a member because we are not in chat.
|
||||
- Do not add QR inviter to groups immediately.
|
||||
- Do not ignore I/O errors in `BlobObject::store_from_base64`.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Rustfmt.
|
||||
|
||||
### Refactor
|
||||
|
||||
- imap: Move resync request from Context to Imap.
|
||||
- Replace imap:: calls in migration 73 with SQL queries.
|
||||
- Remove unused imports.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Readme: update language binding section to avoid usage of cffi in new projects ([#7380](https://github.com/chatmail/core/pull/7380)).
|
||||
- Fix Context::set_stock_translation reference.
|
||||
|
||||
### Tests
|
||||
|
||||
- Test editing saved messages.
|
||||
- Remove ThreadPoolExecutor from test_wait_next_messages.
|
||||
- Move test_two_group_securejoins from receive_imf to securejoin module.
|
||||
- At the end of securejoin Bob has two members in a group chat.
|
||||
- Bob has 0 members in the chat until securejoin finishes.
|
||||
- Do not add QR inviter to groups right after scanning the code.
|
||||
|
||||
## [2.25.0] - 2025-11-05
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Put self-name into group invite codes ([#7398](https://github.com/chatmail/core/pull/7398)).
|
||||
- Slightly nicer and shorter QR and invite codes ([#7390](https://github.com/chatmail/core/pull/7390))
|
||||
|
||||
### Fixes
|
||||
|
||||
- Add device message instead of partial message when receive_imf fails. This fixes a rare bug where the IMAP loop got stuck.
|
||||
- Add info message if user tries to create a QR code for deprecated channel ([#7399](https://github.com/chatmail/core/pull/7399)).
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- deps: Bump actions/upload-artifact from 4 to 5.
|
||||
- deps: Bump actions/download-artifact from 5 to 6.
|
||||
- deps: Bump astral-sh/setup-uv from 7.1.0 to 7.1.2.
|
||||
|
||||
### Refactor
|
||||
|
||||
- sql: Do not expose rusqlite Error type in query_map methods.
|
||||
|
||||
## [2.24.0] - 2025-11-03
|
||||
|
||||
***Note that in v2.24.0, the IMAP loop can get stuck in rare circumstances;
|
||||
use v2.23.0 or v2.25.0 instead.***
|
||||
|
||||
### Documentation
|
||||
|
||||
- Comment why spaced en dash is used to separate message Subject from text.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- [**breaking**] QR codes and symmetric encryption for broadcast channels ([#7268](https://github.com/chatmail/core/pull/7268)).
|
||||
- A new QR type AskJoinBroadcast; cloning a broadcast
|
||||
channel is no longer possible; manually adding a member to a broadcast
|
||||
channel is no longer possible (the only way to join a channel is scanning a QR code or clicking a link)
|
||||
|
||||
### Refactor
|
||||
|
||||
- Split "transport" module out of "login_param".
|
||||
|
||||
## [2.23.0] - 2025-11-01
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Make `dc_chat_is_protected` always return 0.
|
||||
- [**breaking**] Remove public APIs to check if the chat is protected.
|
||||
- [**breaking**] Remove APIs to create protected chats.
|
||||
- [**breaking**] Remove Chat.is_protected().
|
||||
- deltachat-rpc-client: Add Account.add_transport_from_qr() API.
|
||||
- JSON-RPC: add `get_push_state` to check push notification state ([#7356](https://github.com/chatmail/core/pull/7356)).
|
||||
- JSON-RPC: remove unused TypeScript constants ([#7355](https://github.com/chatmail/core/pull/7355)).
|
||||
- Remove `Config::SentboxWatch` ([#7178](https://github.com/chatmail/core/pull/7178)).
|
||||
- Remove `Config::ConfiguredSentboxFolder` and everything related.
|
||||
|
||||
### Build system
|
||||
|
||||
- Ignore configuration for the zed editor ([#7322](https://github.com/chatmail/core/pull/7322)).
|
||||
- nix: Fix build of deltachat-rpc-server-x86_64-darwin.
|
||||
- Update rand to 0.9.
|
||||
- Do not install `pdbpp` in the test environment for CFFI Python bindings.
|
||||
- Migrate from tokio-tar to astral-tokio-tar.
|
||||
- deps: Bump actions/setup-node from 5 to 6.
|
||||
- deps: Bump cachix/install-nix-action from 31.8.0 to 31.8.1.
|
||||
- Fix Rust 1.91.0 lint for derivable Default.
|
||||
|
||||
### CI
|
||||
|
||||
- Pin GitHub action `astral-sh/setup-uv`.
|
||||
- Set 7 days cooldown on Dependabot updates.
|
||||
- Update Rust to 1.91.0.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Document Autocrypt-Gossip `_verified` attribute.
|
||||
|
||||
### Features/Changes
|
||||
|
||||
Metadata reduction:
|
||||
- Protect Autocrypt header.
|
||||
- Anonymize OpenPGP recipients (temorarily disabled due to interoperability problems, see <https://github.com/chatmail/core/issues/7384>).
|
||||
- Protect the `Date` header.
|
||||
|
||||
Onboarding improvements:
|
||||
- Allow plain domain in `dcaccount:` scheme.
|
||||
- Do not resolve MX records during configuration.
|
||||
|
||||
Preparation for multi-transport:
|
||||
- Move the messages only from INBOX and Spam folders.
|
||||
- deltachat-rpc-client: Support multiple transports in resetup_account().
|
||||
|
||||
Various other changes:
|
||||
- Opt-in weekly sending of statistics ([#6851](https://github.com/chatmail/core/pull/6851))
|
||||
- Synchronize encrypted groups creation across devices ([#7001](https://github.com/chatmail/core/pull/7001)).
|
||||
- Do not send Autocrypt in MDNs.
|
||||
- Do not run SecureJoin if we are already in the group.
|
||||
- Show if proxy is enabled in connectivity view ([#7359](https://github.com/chatmail/core/pull/7359)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Don't ignore QR token timestamp from sync messages.
|
||||
- Do not allow sync item timestamps to be in the future.
|
||||
- jsonrpc: Fix `ChatListItem::is_self_in_group`.
|
||||
- Delete obsolete "configured*" keys from `config` table ([#7171](https://github.com/chatmail/core/pull/7171)).
|
||||
- Fix flaky tests::verified_chats::test_verified_chat_editor_reordering and receive_imf::receive_imf_tests::test_two_group_securejoins.
|
||||
- Stop using `leftgrps` table.
|
||||
- Stop notifying about messages in contact request chats.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove invalid Gmail OAuth2 tokens.
|
||||
- Remove ProtectionStatus.
|
||||
- Rename chat::create_group_chat() to create_group().
|
||||
- Remove error stock strings that are rarely used these days ([#7327](https://github.com/chatmail/core/pull/7327)).
|
||||
- Jsonrpc rename change casing in names of jsonrpc structs/enums to comply with rust naming conventions. ([#7324](https://github.com/chatmail/core/pull/7324)).
|
||||
- Stop using deprecated Account.configure().
|
||||
- add_transport_from_qr: Do not set deprecated config values.
|
||||
- sql: Change second query_map function from FnMut to FnOnce.
|
||||
- sql: Add query_map_vec().
|
||||
- sql: Add query_map_collect().
|
||||
- Use rand::fill() instead of rand::rng().fill().
|
||||
- Use SampleString.
|
||||
- Remove unused call to get_credentials().
|
||||
|
||||
### Tests
|
||||
|
||||
- rpc-client: VCard color is the same as the contact color ([#7294](https://github.com/chatmail/core/pull/7294)).
|
||||
- Add unique offsets to ids generated by `TestContext` to increase test correctness ([#7297](https://github.com/chatmail/core/pull/7297)).
|
||||
|
||||
## [2.22.0] - 2025-10-17
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not notify about incoming calls for contact requests and blocked contacts.
|
||||
|
||||
### Tests
|
||||
|
||||
- Accept the chat with the caller before accepting calls.
|
||||
|
||||
## [2.21.0] - 2025-10-16
|
||||
|
||||
### Build system
|
||||
@@ -6957,3 +7179,9 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[2.19.0]: https://github.com/chatmail/core/compare/v2.18.0..v2.19.0
|
||||
[2.20.0]: https://github.com/chatmail/core/compare/v2.19.0..v2.20.0
|
||||
[2.21.0]: https://github.com/chatmail/core/compare/v2.20.0..v2.21.0
|
||||
[2.22.0]: https://github.com/chatmail/core/compare/v2.21.0..v2.22.0
|
||||
[2.23.0]: https://github.com/chatmail/core/compare/v2.22.0..v2.23.0
|
||||
[2.24.0]: https://github.com/chatmail/core/compare/v2.23.0..v2.24.0
|
||||
[2.25.0]: https://github.com/chatmail/core/compare/v2.24.0..v2.25.0
|
||||
[2.26.0]: https://github.com/chatmail/core/compare/v2.25.0..v2.26.0
|
||||
[2.27.0]: https://github.com/chatmail/core/compare/v2.26.0..v2.27.0
|
||||
|
||||
75
Cargo.lock
generated
75
Cargo.lock
generated
@@ -198,6 +198,21 @@ dependencies = [
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "astral-tokio-tar"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec179a06c1769b1e42e1e2cbe74c7dcdb3d6383c838454d063eaac5bbb7ebbe5"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"futures-core",
|
||||
"libc",
|
||||
"portable-atomic",
|
||||
"rustc-hash",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.2"
|
||||
@@ -1289,9 +1304,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "2.21.0"
|
||||
version = "2.27.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"astral-tokio-tar",
|
||||
"async-broadcast",
|
||||
"async-channel 2.5.0",
|
||||
"async-imap",
|
||||
@@ -1316,7 +1332,6 @@ dependencies = [
|
||||
"futures",
|
||||
"futures-lite",
|
||||
"hex",
|
||||
"hickory-resolver",
|
||||
"http-body-util",
|
||||
"humansize",
|
||||
"hyper",
|
||||
@@ -1343,6 +1358,7 @@ dependencies = [
|
||||
"qrcodegen",
|
||||
"quick-xml",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.0",
|
||||
"ratelimit",
|
||||
"regex",
|
||||
"rusqlite",
|
||||
@@ -1367,7 +1383,6 @@ dependencies = [
|
||||
"tokio-io-timeout",
|
||||
"tokio-rustls",
|
||||
"tokio-stream",
|
||||
"tokio-tar",
|
||||
"tokio-util",
|
||||
"toml",
|
||||
"tracing",
|
||||
@@ -1398,7 +1413,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.21.0"
|
||||
version = "2.27.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.5.0",
|
||||
@@ -1420,7 +1435,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "2.21.0"
|
||||
version = "2.27.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1436,7 +1451,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.21.0"
|
||||
version = "2.27.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1465,7 +1480,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.21.0"
|
||||
version = "2.27.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1473,7 +1488,7 @@ dependencies = [
|
||||
"human-panic",
|
||||
"libc",
|
||||
"num-traits",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.0",
|
||||
"serde_json",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
@@ -2014,14 +2029,14 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.23"
|
||||
version = "0.2.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
|
||||
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall 0.4.1",
|
||||
"windows-sys 0.52.0",
|
||||
"libredox",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3270,6 +3285,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"libc",
|
||||
"redox_syscall 0.5.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4859,15 +4875,6 @@ dependencies = [
|
||||
"yasna",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.4.1"
|
||||
@@ -6149,21 +6156,6 @@ dependencies = [
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tar"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d5714c010ca3e5c27114c1cdeb9d14641ace49874aa5626d7149e47aedace75"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"futures-core",
|
||||
"libc",
|
||||
"redox_syscall 0.3.5",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tfo"
|
||||
version = "0.3.1"
|
||||
@@ -7276,17 +7268,6 @@ dependencies = [
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e105d177a3871454f754b33bb0ee637ecaaac997446375fd3e5d43a2ed00c909"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.14",
|
||||
"rustix 0.38.44",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xml-rs"
|
||||
version = "0.8.25"
|
||||
|
||||
15
Cargo.toml
15
Cargo.toml
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "2.21.0"
|
||||
version = "2.27.0"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.85"
|
||||
rust-version = "1.88"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
[profile.dev]
|
||||
@@ -61,7 +61,6 @@ fd-lock = "4"
|
||||
futures-lite = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
hex = "0.4.0"
|
||||
hickory-resolver = "0.25.2"
|
||||
http-body-util = "0.1.3"
|
||||
humansize = "2"
|
||||
hyper = "1"
|
||||
@@ -83,6 +82,7 @@ pgp = { version = "0.17.0", default-features = false }
|
||||
pin-project = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = { version = "0.38", features = ["escape-html"] }
|
||||
rand-old = { package = "rand", version = "0.8" }
|
||||
rand = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||
@@ -104,7 +104,7 @@ thiserror = { workspace = true }
|
||||
tokio-io-timeout = "1.2.1"
|
||||
tokio-rustls = { version = "0.26.2", default-features = false }
|
||||
tokio-stream = { version = "0.1.17", features = ["fs"] }
|
||||
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
||||
astral-tokio-tar = { version = "0.5.6", default-features = false }
|
||||
tokio-util = { workspace = true }
|
||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
toml = "0.9"
|
||||
@@ -156,6 +156,11 @@ name = "receive_emails"
|
||||
required-features = ["internals"]
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "decrypting"
|
||||
required-features = ["internals"]
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "get_chat_msgs"
|
||||
harness = false
|
||||
@@ -187,7 +192,7 @@ log = "0.4"
|
||||
mailparse = "0.16.1"
|
||||
nu-ansi-term = "0.50"
|
||||
num-traits = "0.2"
|
||||
rand = "0.8"
|
||||
rand = "0.9"
|
||||
regex = "1.10"
|
||||
rusqlite = "0.36"
|
||||
sanitize-filename = "0.5"
|
||||
|
||||
@@ -197,12 +197,10 @@ and then run the script.
|
||||
Language bindings are available for:
|
||||
|
||||
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
|
||||
- -> libdeltachat is going to be deprecated and only exists because Android, iOS and Ubuntu Touch are still using it. If you build a new project, then please use the jsonrpc api instead.
|
||||
- **JS**: \[[📂 source](./deltachat-rpc-client) | [📦 npm](https://www.npmjs.com/package/@deltachat/jsonrpc-client) | [📚 docs](https://js.jsonrpc.delta.chat/)\]
|
||||
- **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\]
|
||||
- **Go**
|
||||
- over jsonrpc: \[[📂 source](https://github.com/deltachat/deltachat-rpc-client-go/)\]
|
||||
- over cffi[^1]: \[[📂 source](https://github.com/deltachat/go-deltachat/)\]
|
||||
- **Free Pascal**[^1] \[[📂 source](https://github.com/deltachat/deltachat-fp/)\]
|
||||
- **Go** \[[📂 source](https://github.com/deltachat/deltachat-rpc-client-go/)\]
|
||||
- **Java** and **Swift** (contained in the Android/iOS repos)
|
||||
|
||||
The following "frontend" projects make use of the Rust-library
|
||||
@@ -215,5 +213,3 @@ or its language bindings:
|
||||
- [Telepathy](https://code.ur.gs/lupine/telepathy-padfoot/)
|
||||
- [Ubuntu Touch](https://codeberg.org/lk108/deltatouch)
|
||||
- several **Bots**
|
||||
|
||||
[^1]: Out of date / unmaintained, if you like those languages feel free to start maintaining them. If you have questions we'll help you, please ask in the issues.
|
||||
|
||||
200
benches/decrypting.rs
Normal file
200
benches/decrypting.rs
Normal file
@@ -0,0 +1,200 @@
|
||||
//! Benchmarks for message decryption,
|
||||
//! comparing decryption of symmetrically-encrypted messages
|
||||
//! to decryption of asymmetrically-encrypted messages.
|
||||
//!
|
||||
//! Call with
|
||||
//!
|
||||
//! ```text
|
||||
//! cargo bench --bench decrypting --features="internals"
|
||||
//! ```
|
||||
//!
|
||||
//! or, if you want to only run e.g. the 'Decrypt a symmetrically encrypted message' benchmark:
|
||||
//!
|
||||
//! ```text
|
||||
//! cargo bench --bench decrypting --features="internals" -- 'Decrypt a symmetrically encrypted message'
|
||||
//! ```
|
||||
//!
|
||||
//! You can also pass a substring.
|
||||
//! So, you can run all 'Decrypt and parse' benchmarks with:
|
||||
//!
|
||||
//! ```text
|
||||
//! cargo bench --bench decrypting --features="internals" -- 'Decrypt and parse'
|
||||
//! ```
|
||||
//!
|
||||
//! Symmetric decryption has to try out all known secrets,
|
||||
//! You can benchmark this by adapting the `NUM_SECRETS` variable.
|
||||
|
||||
use std::hint::black_box;
|
||||
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
use deltachat::internals_for_benches::create_broadcast_secret;
|
||||
use deltachat::internals_for_benches::create_dummy_keypair;
|
||||
use deltachat::internals_for_benches::save_broadcast_secret;
|
||||
use deltachat::{
|
||||
Events,
|
||||
chat::ChatId,
|
||||
config::Config,
|
||||
context::Context,
|
||||
internals_for_benches::key_from_asc,
|
||||
internals_for_benches::parse_and_get_text,
|
||||
internals_for_benches::store_self_keypair,
|
||||
pgp::{KeyPair, decrypt, pk_encrypt, symm_encrypt_message},
|
||||
stock_str::StockStrings,
|
||||
};
|
||||
use rand::{Rng, rng};
|
||||
use tempfile::tempdir;
|
||||
|
||||
const NUM_SECRETS: usize = 500;
|
||||
|
||||
async fn create_context() -> Context {
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
let context = Context::new(dbfile.as_path(), 100, Events::new(), StockStrings::new())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
context
|
||||
.set_config(Config::ConfiguredAddr, Some("bob@example.net"))
|
||||
.await
|
||||
.unwrap();
|
||||
let secret = key_from_asc(include_str!("../test-data/key/bob-secret.asc")).unwrap();
|
||||
let public = secret.signed_public_key();
|
||||
let key_pair = KeyPair { public, secret };
|
||||
store_self_keypair(&context, &key_pair)
|
||||
.await
|
||||
.expect("Failed to save key");
|
||||
|
||||
context
|
||||
}
|
||||
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("Decrypt");
|
||||
|
||||
// ===========================================================================================
|
||||
// Benchmarks for decryption only, without any other parsing
|
||||
// ===========================================================================================
|
||||
|
||||
group.sample_size(10);
|
||||
|
||||
group.bench_function("Decrypt a symmetrically encrypted message", |b| {
|
||||
let plain = generate_plaintext();
|
||||
let secrets = generate_secrets();
|
||||
let encrypted = tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||
let secret = secrets[NUM_SECRETS / 2].clone();
|
||||
symm_encrypt_message(
|
||||
plain.clone(),
|
||||
create_dummy_keypair("alice@example.org").unwrap().secret,
|
||||
black_box(&secret),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
b.iter(|| {
|
||||
let mut msg =
|
||||
decrypt(encrypted.clone().into_bytes(), &[], black_box(&secrets)).unwrap();
|
||||
let decrypted = msg.as_data_vec().unwrap();
|
||||
|
||||
assert_eq!(black_box(decrypted), plain);
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("Decrypt a public-key encrypted message", |b| {
|
||||
let plain = generate_plaintext();
|
||||
let key_pair = create_dummy_keypair("alice@example.org").unwrap();
|
||||
let secrets = generate_secrets();
|
||||
let encrypted = tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||
pk_encrypt(
|
||||
plain.clone(),
|
||||
vec![black_box(key_pair.public.clone())],
|
||||
Some(key_pair.secret.clone()),
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
b.iter(|| {
|
||||
let mut msg = decrypt(
|
||||
encrypted.clone().into_bytes(),
|
||||
std::slice::from_ref(&key_pair.secret),
|
||||
black_box(&secrets),
|
||||
)
|
||||
.unwrap();
|
||||
let decrypted = msg.as_data_vec().unwrap();
|
||||
|
||||
assert_eq!(black_box(decrypted), plain);
|
||||
});
|
||||
});
|
||||
|
||||
// ===========================================================================================
|
||||
// Benchmarks for the whole parsing pipeline, incl. decryption (but excl. receive_imf())
|
||||
// ===========================================================================================
|
||||
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let mut secrets = generate_secrets();
|
||||
|
||||
// "secret" is the shared secret that was used to encrypt text_symmetrically_encrypted.eml.
|
||||
// Put it into the middle of our secrets:
|
||||
secrets[NUM_SECRETS / 2] = "secret".to_string();
|
||||
|
||||
let context = rt.block_on(async {
|
||||
let context = create_context().await;
|
||||
for (i, secret) in secrets.iter().enumerate() {
|
||||
save_broadcast_secret(&context, ChatId::new(10 + i as u32), secret)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
context
|
||||
});
|
||||
|
||||
group.bench_function("Decrypt and parse a symmetrically encrypted message", |b| {
|
||||
b.to_async(&rt).iter(|| {
|
||||
let ctx = context.clone();
|
||||
async move {
|
||||
let text = parse_and_get_text(
|
||||
&ctx,
|
||||
include_bytes!("../test-data/message/text_symmetrically_encrypted.eml"),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(text, "Symmetrically encrypted message");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("Decrypt and parse a public-key encrypted message", |b| {
|
||||
b.to_async(&rt).iter(|| {
|
||||
let ctx = context.clone();
|
||||
async move {
|
||||
let text = parse_and_get_text(
|
||||
&ctx,
|
||||
include_bytes!("../test-data/message/text_from_alice_encrypted.eml"),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(text, "hi");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn generate_secrets() -> Vec<String> {
|
||||
let secrets: Vec<String> = (0..NUM_SECRETS)
|
||||
.map(|_| create_broadcast_secret())
|
||||
.collect();
|
||||
secrets
|
||||
}
|
||||
|
||||
fn generate_plaintext() -> Vec<u8> {
|
||||
let mut plain: Vec<u8> = vec![0; 500];
|
||||
rng().fill(&mut plain[..]);
|
||||
plain
|
||||
}
|
||||
|
||||
criterion_group!(benches, criterion_benchmark);
|
||||
criterion_main!(benches);
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.21.0"
|
||||
version = "2.27.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -1763,9 +1763,7 @@ dc_chat_t* dc_get_chat (dc_context_t* context, uint32_t ch
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param protect If set to 1 the function creates group with protection initially enabled.
|
||||
* Only verified members are allowed in these groups
|
||||
* and end-to-end-encryption is always enabled.
|
||||
* @param protect Deprecated 2025-08-31, ignored.
|
||||
* @param name The name of the group chat to create.
|
||||
* The name may be changed later using dc_set_chat_name().
|
||||
* To find out the name of a group later, see dc_chat_get_name()
|
||||
@@ -2565,6 +2563,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
|
||||
#define DC_QR_ASK_VERIFYCONTACT 200 // id=contact
|
||||
#define DC_QR_ASK_VERIFYGROUP 202 // text1=groupname
|
||||
#define DC_QR_ASK_VERIFYBROADCAST 204 // text1=broadcast name
|
||||
#define DC_QR_FPR_OK 210 // id=contact
|
||||
#define DC_QR_FPR_MISMATCH 220 // id=contact
|
||||
#define DC_QR_FPR_WITHOUT_ADDR 230 // test1=formatted fingerprint
|
||||
@@ -2579,8 +2578,10 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
#define DC_QR_ERROR 400 // text1=error string
|
||||
#define DC_QR_WITHDRAW_VERIFYCONTACT 500
|
||||
#define DC_QR_WITHDRAW_VERIFYGROUP 502 // text1=groupname
|
||||
#define DC_QR_WITHDRAW_JOINBROADCAST 504 // text1=broadcast name
|
||||
#define DC_QR_REVIVE_VERIFYCONTACT 510
|
||||
#define DC_QR_REVIVE_VERIFYGROUP 512 // text1=groupname
|
||||
#define DC_QR_REVIVE_JOINBROADCAST 514 // text1=broadcast name
|
||||
#define DC_QR_LOGIN 520 // text1=email_address
|
||||
|
||||
/**
|
||||
@@ -2597,8 +2598,9 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
* ask whether to verify the contact;
|
||||
* if so, start the protocol with dc_join_securejoin().
|
||||
*
|
||||
* - DC_QR_ASK_VERIFYGROUP with dc_lot_t::text1=Group name:
|
||||
* ask whether to join the group;
|
||||
* - DC_QR_ASK_VERIFYGROUP or DC_QR_ASK_VERIFYBROADCAST
|
||||
* with dc_lot_t::text1=Group name:
|
||||
* ask whether to join the chat;
|
||||
* if so, start the protocol with dc_join_securejoin().
|
||||
*
|
||||
* - DC_QR_FPR_OK with dc_lot_t::id=Contact ID:
|
||||
@@ -2681,7 +2683,8 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
|
||||
* Get QR code text that will offer an Setup-Contact or Verified-Group invitation.
|
||||
*
|
||||
* The scanning device will pass the scanned content to dc_check_qr() then;
|
||||
* if dc_check_qr() returns DC_QR_ASK_VERIFYCONTACT or DC_QR_ASK_VERIFYGROUP
|
||||
* if dc_check_qr() returns
|
||||
* DC_QR_ASK_VERIFYCONTACT, DC_QR_ASK_VERIFYGROUP or DC_QR_ASK_VERIFYBROADCAST
|
||||
* an out-of-band-verification can be joined using dc_join_securejoin()
|
||||
*
|
||||
* The returned text will also work as a normal https:-link,
|
||||
@@ -2722,7 +2725,7 @@ char* dc_get_securejoin_qr_svg (dc_context_t* context, uint32_
|
||||
* Continue a Setup-Contact or Verified-Group-Invite protocol
|
||||
* started on another device with dc_get_securejoin_qr().
|
||||
* This function is typically called when dc_check_qr() returns
|
||||
* lot.state=DC_QR_ASK_VERIFYCONTACT or lot.state=DC_QR_ASK_VERIFYGROUP.
|
||||
* lot.state=DC_QR_ASK_VERIFYCONTACT, lot.state=DC_QR_ASK_VERIFYGROUP or lot.state=DC_QR_ASK_VERIFYBROADCAST
|
||||
*
|
||||
* The function returns immediately and the handshake runs in background,
|
||||
* sending and receiving several messages.
|
||||
@@ -3295,12 +3298,30 @@ void dc_accounts_maybe_network_lost (dc_accounts_t* accounts);
|
||||
* without forgetting to create notifications caused by timing race conditions.
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param accounts The account manager as created by dc_accounts_new().
|
||||
* @param timeout The timeout in seconds
|
||||
* @return Return 1 if DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE was emitted and 0 otherwise.
|
||||
*/
|
||||
int dc_accounts_background_fetch (dc_accounts_t* accounts, uint64_t timeout);
|
||||
|
||||
|
||||
/**
|
||||
* Stop ongoing background fetch.
|
||||
*
|
||||
* Calling this function allows to stop dc_accounts_background_fetch() early.
|
||||
* dc_accounts_background_fetch() will then return immediately
|
||||
* and emit DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE unless
|
||||
* if it has failed and returned 0.
|
||||
*
|
||||
* If there is no ongoing dc_accounts_background_fetch() call,
|
||||
* calling this function does nothing.
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param accounts The account manager as created by dc_accounts_new().
|
||||
*/
|
||||
void dc_accounts_stop_background_fetch (dc_accounts_t *accounts);
|
||||
|
||||
|
||||
/**
|
||||
* Sets device token for Apple Push Notification service.
|
||||
* Returns immediately.
|
||||
@@ -3890,18 +3911,12 @@ int dc_chat_can_send (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Check if a chat is protected.
|
||||
*
|
||||
* Only verified contacts
|
||||
* as determined by dc_contact_is_verified()
|
||||
* can be added to protected chats.
|
||||
*
|
||||
* Protected chats are created using dc_create_group_chat()
|
||||
* by setting the 'protect' parameter to 1.
|
||||
* Deprecated, always returns 0.
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
* @param chat The chat object.
|
||||
* @return 1=chat protected, 0=chat is not protected.
|
||||
* @return Always 0.
|
||||
* @deprecated 2025-09-09
|
||||
*/
|
||||
int dc_chat_is_protected (const dc_chat_t* chat);
|
||||
|
||||
@@ -5350,11 +5365,9 @@ dc_provider_t* dc_provider_new_from_email (const dc_context_t* conte
|
||||
|
||||
|
||||
/**
|
||||
* Create a provider struct for the given e-mail address by local and DNS lookup.
|
||||
* Create a provider struct for the given e-mail address by local lookup.
|
||||
*
|
||||
* First lookup is done from the local database as of dc_provider_new_from_email().
|
||||
* If the first lookup fails, an additional DNS lookup is done,
|
||||
* trying to figure out the provider belonging to custom domains.
|
||||
* DNS lookup is not used anymore and this function is deprecated.
|
||||
*
|
||||
* @memberof dc_provider_t
|
||||
* @param context The context object.
|
||||
@@ -5362,6 +5375,7 @@ dc_provider_t* dc_provider_new_from_email (const dc_context_t* conte
|
||||
* @return A dc_provider_t struct which can be used with the dc_provider_get_*
|
||||
* accessor functions. If no provider info is found, NULL will be
|
||||
* returned.
|
||||
* @deprecated 2025-10-17 use dc_provider_new_from_email() instead.
|
||||
*/
|
||||
dc_provider_t* dc_provider_new_from_email_with_dns (const dc_context_t* context, const char* email);
|
||||
|
||||
@@ -6965,11 +6979,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used to build the string returned by dc_get_contact_encrinfo().
|
||||
#define DC_STR_ENCR_NONE 28
|
||||
|
||||
/// "This message was encrypted for another setup."
|
||||
///
|
||||
/// Used as message text if decryption fails.
|
||||
#define DC_STR_CANTDECRYPT_MSG_BODY 29
|
||||
|
||||
/// "Fingerprints"
|
||||
///
|
||||
/// Used to build the string returned by dc_get_contact_encrinfo().
|
||||
@@ -7529,14 +7538,13 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
/// "You set message deletion timer to 1 minute."
|
||||
///
|
||||
/// Used in status messages.
|
||||
/// @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.
|
||||
///
|
||||
/// Used in status messages.
|
||||
/// @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."
|
||||
@@ -7687,12 +7695,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// `%1$s` will be replaced by the provider's domain.
|
||||
#define DC_STR_INVALID_UNENCRYPTED_MAIL 174
|
||||
|
||||
/// "⚠️ It seems you are using Delta Chat on multiple devices that cannot decrypt each other's outgoing messages. To fix this, on the older device use \"Settings / Add Second Device\" and follow the instructions."
|
||||
///
|
||||
/// Added to the device chat if could not decrypt a new outgoing message (i.e. not when fetching
|
||||
/// existing messages). But no more than once a day.
|
||||
#define DC_STR_CANT_DECRYPT_OUTGOING_MSGS 175
|
||||
|
||||
/// "You reacted %1$s to '%2$s'"
|
||||
///
|
||||
/// `%1$s` will be replaced by the reaction, usually an emoji
|
||||
@@ -7756,6 +7758,21 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// `%1$s` will be replaced with the channel name.
|
||||
#define DC_STR_SECURE_JOIN_CHANNEL_QR_DESC 201
|
||||
|
||||
/// "Proxy Enabled"
|
||||
///
|
||||
/// Title for proxy section in connectivity view.
|
||||
#define DC_STR_PROXY_ENABLED 220
|
||||
|
||||
/// "You are using a proxy. If you're having trouble connecting, try a different proxy."
|
||||
///
|
||||
/// Description in connectivity view when proxy is enabled.
|
||||
#define DC_STR_PROXY_ENABLED_DESCRIPTION 221
|
||||
|
||||
/// "Messages in this chat use classic email and are not encrypted."
|
||||
///
|
||||
/// Used as the first info messages in newly created classic email threads.
|
||||
#define DC_STR_CHAT_UNENCRYPTED_EXPLANATON 230
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
@@ -22,7 +22,7 @@ use std::sync::{Arc, LazyLock};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use deltachat::chat::{ChatId, ChatVisibility, MessageListOptions, MuteDuration, ProtectionStatus};
|
||||
use deltachat::chat::{ChatId, ChatVisibility, MessageListOptions, MuteDuration};
|
||||
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
|
||||
use deltachat::contact::{Contact, ContactId, Origin};
|
||||
use deltachat::context::{Context, ContextBuilder};
|
||||
@@ -39,7 +39,6 @@ use deltachat_jsonrpc::api::CommandApi;
|
||||
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
|
||||
use message::Viewtype;
|
||||
use num_traits::{FromPrimitive, ToPrimitive};
|
||||
use rand::Rng;
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::task::JoinHandle;
|
||||
@@ -101,7 +100,7 @@ pub unsafe extern "C" fn dc_context_new(
|
||||
|
||||
let ctx = if blobdir.is_null() || *blobdir == 0 {
|
||||
// generate random ID as this functionality is not yet available on the C-api.
|
||||
let id = rand::thread_rng().gen();
|
||||
let id = rand::random();
|
||||
block_on(
|
||||
ContextBuilder::new(as_path(dbfile).to_path_buf())
|
||||
.with_id(id)
|
||||
@@ -129,7 +128,7 @@ pub unsafe extern "C" fn dc_context_new_closed(dbfile: *const libc::c_char) -> *
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let id = rand::thread_rng().gen();
|
||||
let id = rand::random();
|
||||
match block_on(
|
||||
ContextBuilder::new(as_path(dbfile).to_path_buf())
|
||||
.with_id(id)
|
||||
@@ -1721,7 +1720,7 @@ pub unsafe extern "C" fn dc_get_chat(context: *mut dc_context_t, chat_id: u32) -
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_create_group_chat(
|
||||
context: *mut dc_context_t,
|
||||
protect: libc::c_int,
|
||||
_protect: libc::c_int,
|
||||
name: *const libc::c_char,
|
||||
) -> u32 {
|
||||
if context.is_null() || name.is_null() {
|
||||
@@ -1729,22 +1728,12 @@ pub unsafe extern "C" fn dc_create_group_chat(
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
let Some(protect) = ProtectionStatus::from_i32(protect)
|
||||
.context("Bad protect-value for dc_create_group_chat()")
|
||||
.log_err(ctx)
|
||||
.ok()
|
||||
else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
block_on(async move {
|
||||
chat::create_group_chat(ctx, protect, &to_string_lossy(name))
|
||||
.await
|
||||
.context("Failed to create group chat")
|
||||
.log_err(ctx)
|
||||
.map(|id| id.to_u32())
|
||||
.unwrap_or(0)
|
||||
})
|
||||
block_on(chat::create_group(ctx, &to_string_lossy(name)))
|
||||
.context("Failed to create group chat")
|
||||
.log_err(ctx)
|
||||
.map(|id| id.to_u32())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3206,13 +3195,8 @@ pub unsafe extern "C" fn dc_chat_can_send(chat: *mut dc_chat_t) -> libc::c_int {
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_chat_is_protected(chat: *mut dc_chat_t) -> libc::c_int {
|
||||
if chat.is_null() {
|
||||
eprintln!("ignoring careless call to dc_chat_is_protected()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_chat = &*chat;
|
||||
ffi_chat.chat.is_protected() as libc::c_int
|
||||
pub extern "C" fn dc_chat_is_protected(_chat: *mut dc_chat_t) -> libc::c_int {
|
||||
0
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -4256,7 +4240,17 @@ pub unsafe extern "C" fn dc_contact_get_color(contact: *mut dc_contact_t) -> u32
|
||||
return 0;
|
||||
}
|
||||
let ffi_contact = &*contact;
|
||||
ffi_contact.contact.get_color()
|
||||
let ctx = &*ffi_contact.context;
|
||||
block_on(async move {
|
||||
ffi_contact
|
||||
.contact
|
||||
// We don't want any UIs displaying gray self-color.
|
||||
.get_or_gen_color(ctx)
|
||||
.await
|
||||
.context("Contact::get_color()")
|
||||
.log_err(ctx)
|
||||
.unwrap_or(0)
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -4661,13 +4655,9 @@ pub unsafe extern "C" fn dc_provider_new_from_email(
|
||||
|
||||
let ctx = &*context;
|
||||
|
||||
match block_on(provider::get_provider_info_by_addr(
|
||||
ctx,
|
||||
addr.as_str(),
|
||||
true,
|
||||
))
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
match provider::get_provider_info_by_addr(addr.as_str())
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
Some(provider) => provider,
|
||||
None => ptr::null_mut(),
|
||||
@@ -4686,25 +4676,13 @@ pub unsafe extern "C" fn dc_provider_new_from_email_with_dns(
|
||||
let addr = to_string_lossy(addr);
|
||||
|
||||
let ctx = &*context;
|
||||
let proxy_enabled = block_on(ctx.get_config_bool(config::Config::ProxyEnabled))
|
||||
.context("Can't get config")
|
||||
.log_err(ctx);
|
||||
|
||||
match proxy_enabled {
|
||||
Ok(proxy_enabled) => {
|
||||
match block_on(provider::get_provider_info_by_addr(
|
||||
ctx,
|
||||
addr.as_str(),
|
||||
proxy_enabled,
|
||||
))
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
Some(provider) => provider,
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
Err(_) => ptr::null_mut(),
|
||||
match provider::get_provider_info_by_addr(addr.as_str())
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
Some(provider) => provider,
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5049,6 +5027,17 @@ pub unsafe extern "C" fn dc_accounts_background_fetch(
|
||||
1
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_stop_background_fetch(accounts: *mut dc_accounts_t) {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_stop_background_fetch()");
|
||||
return;
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
block_on(accounts.read()).stop_background_fetch();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_set_push_device_token(
|
||||
accounts: *mut dc_accounts_t,
|
||||
|
||||
@@ -45,6 +45,7 @@ impl Lot {
|
||||
Self::Qr(qr) => match qr {
|
||||
Qr::AskVerifyContact { .. } => None,
|
||||
Qr::AskVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
|
||||
Qr::AskJoinBroadcast { name, .. } => Some(Cow::Borrowed(name)),
|
||||
Qr::FprOk { .. } => None,
|
||||
Qr::FprMismatch { .. } => None,
|
||||
Qr::FprWithoutAddr { fingerprint, .. } => Some(Cow::Borrowed(fingerprint)),
|
||||
@@ -57,8 +58,10 @@ impl Lot {
|
||||
Qr::Text { text } => Some(Cow::Borrowed(text)),
|
||||
Qr::WithdrawVerifyContact { .. } => None,
|
||||
Qr::WithdrawVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
|
||||
Qr::WithdrawJoinBroadcast { name, .. } => Some(Cow::Borrowed(name)),
|
||||
Qr::ReviveVerifyContact { .. } => None,
|
||||
Qr::ReviveVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
|
||||
Qr::ReviveJoinBroadcast { name, .. } => Some(Cow::Borrowed(name)),
|
||||
Qr::Login { address, .. } => Some(Cow::Borrowed(address)),
|
||||
},
|
||||
Self::Error(err) => Some(Cow::Borrowed(err)),
|
||||
@@ -98,6 +101,7 @@ impl Lot {
|
||||
Self::Qr(qr) => match qr {
|
||||
Qr::AskVerifyContact { .. } => LotState::QrAskVerifyContact,
|
||||
Qr::AskVerifyGroup { .. } => LotState::QrAskVerifyGroup,
|
||||
Qr::AskJoinBroadcast { .. } => LotState::QrAskJoinBroadcast,
|
||||
Qr::FprOk { .. } => LotState::QrFprOk,
|
||||
Qr::FprMismatch { .. } => LotState::QrFprMismatch,
|
||||
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
|
||||
@@ -110,8 +114,10 @@ impl Lot {
|
||||
Qr::Text { .. } => LotState::QrText,
|
||||
Qr::WithdrawVerifyContact { .. } => LotState::QrWithdrawVerifyContact,
|
||||
Qr::WithdrawVerifyGroup { .. } => LotState::QrWithdrawVerifyGroup,
|
||||
Qr::WithdrawJoinBroadcast { .. } => LotState::QrWithdrawJoinBroadcast,
|
||||
Qr::ReviveVerifyContact { .. } => LotState::QrReviveVerifyContact,
|
||||
Qr::ReviveVerifyGroup { .. } => LotState::QrReviveVerifyGroup,
|
||||
Qr::ReviveJoinBroadcast { .. } => LotState::QrReviveJoinBroadcast,
|
||||
Qr::Login { .. } => LotState::QrLogin,
|
||||
},
|
||||
Self::Error(_err) => LotState::QrError,
|
||||
@@ -124,6 +130,7 @@ impl Lot {
|
||||
Self::Qr(qr) => match qr {
|
||||
Qr::AskVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
||||
Qr::AskVerifyGroup { .. } => Default::default(),
|
||||
Qr::AskJoinBroadcast { .. } => Default::default(),
|
||||
Qr::FprOk { contact_id } => contact_id.to_u32(),
|
||||
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default().to_u32(),
|
||||
Qr::FprWithoutAddr { .. } => Default::default(),
|
||||
@@ -135,9 +142,11 @@ impl Lot {
|
||||
Qr::Url { .. } => Default::default(),
|
||||
Qr::Text { .. } => Default::default(),
|
||||
Qr::WithdrawVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
||||
Qr::WithdrawVerifyGroup { .. } => Default::default(),
|
||||
Qr::WithdrawVerifyGroup { .. } | Qr::WithdrawJoinBroadcast { .. } => {
|
||||
Default::default()
|
||||
}
|
||||
Qr::ReviveVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
||||
Qr::ReviveVerifyGroup { .. } => Default::default(),
|
||||
Qr::ReviveVerifyGroup { .. } | Qr::ReviveJoinBroadcast { .. } => Default::default(),
|
||||
Qr::Login { .. } => Default::default(),
|
||||
},
|
||||
Self::Error(_) => Default::default(),
|
||||
@@ -166,6 +175,9 @@ pub enum LotState {
|
||||
/// text1=groupname
|
||||
QrAskVerifyGroup = 202,
|
||||
|
||||
/// text1=broadcast_name
|
||||
QrAskJoinBroadcast = 204,
|
||||
|
||||
/// id=contact
|
||||
QrFprOk = 210,
|
||||
|
||||
@@ -201,11 +213,15 @@ pub enum LotState {
|
||||
|
||||
/// text1=groupname
|
||||
QrWithdrawVerifyGroup = 502,
|
||||
/// text1=broadcast channel name
|
||||
QrWithdrawJoinBroadcast = 504,
|
||||
|
||||
QrReviveVerifyContact = 510,
|
||||
|
||||
/// text1=groupname
|
||||
QrReviveVerifyGroup = 512,
|
||||
/// text1=groupname
|
||||
QrReviveJoinBroadcast = 514,
|
||||
|
||||
/// text1=email_address
|
||||
QrLogin = 520,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.21.0"
|
||||
version = "2.27.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -12,7 +12,6 @@ use deltachat::calls::ice_servers;
|
||||
use deltachat::chat::{
|
||||
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
|
||||
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
|
||||
ProtectionStatus,
|
||||
};
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::config::Config;
|
||||
@@ -54,20 +53,21 @@ use types::contact::{ContactObject, VcardContact};
|
||||
use types::events::Event;
|
||||
use types::http::HttpResponse;
|
||||
use types::message::{MessageData, MessageObject, MessageReadReceipt};
|
||||
use types::notify_state::JsonrpcNotifyState;
|
||||
use types::provider_info::ProviderInfo;
|
||||
use types::reactions::JSONRPCReactions;
|
||||
use types::reactions::JsonrpcReactions;
|
||||
use types::webxdc::WebxdcMessageInfo;
|
||||
|
||||
use self::types::message::{MessageInfo, MessageLoadResult};
|
||||
use self::types::{
|
||||
chat::{BasicChat, JSONRPCChatVisibility, MuteDuration},
|
||||
chat::{BasicChat, JsonrpcChatVisibility, MuteDuration},
|
||||
location::JsonrpcLocation,
|
||||
message::{
|
||||
JSONRPCMessageListItem, MessageNotificationInfo, MessageSearchResult, MessageViewtype,
|
||||
JsonrpcMessageListItem, MessageNotificationInfo, MessageSearchResult, MessageViewtype,
|
||||
},
|
||||
};
|
||||
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
|
||||
use crate::api::types::qr::QrObject;
|
||||
use crate::api::types::qr::{QrObject, SecurejoinSource, SecurejoinUiPath};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct AccountState {
|
||||
@@ -273,7 +273,7 @@ impl CommandApi {
|
||||
/// The `AccountsBackgroundFetchDone` event is emitted at the end even in case of timeout.
|
||||
/// Process all events until you get this one and you can safely return to the background
|
||||
/// without forgetting to create notifications caused by timing race conditions.
|
||||
async fn accounts_background_fetch(&self, timeout_in_seconds: f64) -> Result<()> {
|
||||
async fn background_fetch(&self, timeout_in_seconds: f64) -> Result<()> {
|
||||
let future = {
|
||||
let lock = self.accounts.read().await;
|
||||
lock.background_fetch(std::time::Duration::from_secs_f64(timeout_in_seconds))
|
||||
@@ -283,6 +283,11 @@ impl CommandApi {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stop_background_fetch(&self) -> Result<()> {
|
||||
self.accounts.read().await.stop_background_fetch();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// Methods that work on individual accounts
|
||||
// ---------------------------------------------
|
||||
@@ -313,6 +318,12 @@ impl CommandApi {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current push notification state.
|
||||
async fn get_push_state(&self, account_id: u32) -> Result<JsonrpcNotifyState> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
Ok(ctx.push_state().await.into())
|
||||
}
|
||||
|
||||
/// Get the combined filesize of an account in bytes
|
||||
async fn get_account_file_size(&self, account_id: u32) -> Result<u64> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
@@ -336,21 +347,10 @@ impl CommandApi {
|
||||
/// instead of the domain.
|
||||
async fn get_provider_info(
|
||||
&self,
|
||||
account_id: u32,
|
||||
_account_id: u32,
|
||||
email: String,
|
||||
) -> Result<Option<ProviderInfo>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
|
||||
let proxy_enabled = ctx
|
||||
.get_config_bool(deltachat::config::Config::ProxyEnabled)
|
||||
.await?;
|
||||
|
||||
let provider_info = get_provider_info(
|
||||
&ctx,
|
||||
email.split('@').next_back().unwrap_or(""),
|
||||
proxy_enabled,
|
||||
)
|
||||
.await;
|
||||
let provider_info = get_provider_info(email.split('@').next_back().unwrap_or(""));
|
||||
Ok(ProviderInfo::from_dc_type(provider_info))
|
||||
}
|
||||
|
||||
@@ -393,11 +393,6 @@ impl CommandApi {
|
||||
Ok(BlobObject::create_and_deduplicate(&ctx, file, file)?.to_abs_path())
|
||||
}
|
||||
|
||||
async fn draft_self_report(&self, account_id: u32) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
Ok(ctx.draft_self_report().await?.to_u32())
|
||||
}
|
||||
|
||||
/// Sets the given configuration key.
|
||||
async fn set_config(&self, account_id: u32, key: String, value: Option<String>) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
@@ -896,6 +891,38 @@ impl CommandApi {
|
||||
Ok(chat_id.to_u32())
|
||||
}
|
||||
|
||||
/// Like `secure_join()`, but allows to pass a source and a UI-path.
|
||||
/// You only need this if your UI has an option to send statistics
|
||||
/// to Delta Chat's developers.
|
||||
///
|
||||
/// **source**: The source where the QR code came from.
|
||||
/// E.g. a link that was clicked inside or outside Delta Chat,
|
||||
/// the "Paste from Clipboard" action,
|
||||
/// the "Load QR code as image" action,
|
||||
/// or a QR code scan.
|
||||
///
|
||||
/// **uipath**: Which UI path did the user use to arrive at the QR code screen.
|
||||
/// If the SecurejoinSource was ExternalLink or InternalLink,
|
||||
/// pass `None` here, because the QR code screen wasn't even opened.
|
||||
/// ```
|
||||
async fn secure_join_with_ux_info(
|
||||
&self,
|
||||
account_id: u32,
|
||||
qr: String,
|
||||
source: Option<SecurejoinSource>,
|
||||
uipath: Option<SecurejoinUiPath>,
|
||||
) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let chat_id = securejoin::join_securejoin_with_ux_info(
|
||||
&ctx,
|
||||
&qr,
|
||||
source.map(Into::into),
|
||||
uipath.map(Into::into),
|
||||
)
|
||||
.await?;
|
||||
Ok(chat_id.to_u32())
|
||||
}
|
||||
|
||||
async fn leave_group(&self, account_id: u32, chat_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
remove_contact_from_chat(&ctx, ChatId::new(chat_id), ContactId::SELF).await
|
||||
@@ -979,17 +1006,16 @@ impl CommandApi {
|
||||
/// To check, if a chat is still unpromoted, you can look at the `is_unpromoted` property of `BasicChat` or `FullChat`.
|
||||
/// This may be useful if you want to show some help for just created groups.
|
||||
///
|
||||
/// @param protect If set to 1 the function creates group with protection initially enabled.
|
||||
/// Only verified members are allowed in these groups
|
||||
async fn create_group_chat(&self, account_id: u32, name: String, protect: bool) -> Result<u32> {
|
||||
/// `protect` argument is deprecated as of 2025-10-22 and is left for compatibility.
|
||||
/// Pass `false` here.
|
||||
async fn create_group_chat(
|
||||
&self,
|
||||
account_id: u32,
|
||||
name: String,
|
||||
_protect: bool,
|
||||
) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let protect = match protect {
|
||||
true => ProtectionStatus::Protected,
|
||||
false => ProtectionStatus::Unprotected,
|
||||
};
|
||||
chat::create_group_ex(&ctx, Some(protect), &name)
|
||||
.await
|
||||
.map(|id| id.to_u32())
|
||||
chat::create_group(&ctx, &name).await.map(|id| id.to_u32())
|
||||
}
|
||||
|
||||
/// Create a new unencrypted group chat.
|
||||
@@ -998,7 +1024,7 @@ impl CommandApi {
|
||||
/// address-contacts.
|
||||
async fn create_group_chat_unencrypted(&self, account_id: u32, name: String) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
chat::create_group_ex(&ctx, None, &name)
|
||||
chat::create_group_unencrypted(&ctx, &name)
|
||||
.await
|
||||
.map(|id| id.to_u32())
|
||||
}
|
||||
@@ -1009,7 +1035,7 @@ impl CommandApi {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Create a new **broadcast channel**
|
||||
/// Create a new, outgoing **broadcast channel**
|
||||
/// (called "Channel" in the UI).
|
||||
///
|
||||
/// Broadcast channels are similar to groups on the sending device,
|
||||
@@ -1070,7 +1096,7 @@ impl CommandApi {
|
||||
&self,
|
||||
account_id: u32,
|
||||
chat_id: u32,
|
||||
visibility: JSONRPCChatVisibility,
|
||||
visibility: JsonrpcChatVisibility,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
|
||||
@@ -1275,7 +1301,7 @@ impl CommandApi {
|
||||
chat_id: u32,
|
||||
info_only: bool,
|
||||
add_daymarker: bool,
|
||||
) -> Result<Vec<JSONRPCMessageListItem>> {
|
||||
) -> Result<Vec<JsonrpcMessageListItem>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let msg = get_chat_msgs_ex(
|
||||
&ctx,
|
||||
@@ -1289,7 +1315,7 @@ impl CommandApi {
|
||||
Ok(msg
|
||||
.iter()
|
||||
.map(|chat_item| (*chat_item).into())
|
||||
.collect::<Vec<JSONRPCMessageListItem>>())
|
||||
.collect::<Vec<JsonrpcMessageListItem>>())
|
||||
}
|
||||
|
||||
async fn get_message(&self, account_id: u32, msg_id: u32) -> Result<MessageObject> {
|
||||
@@ -2210,7 +2236,7 @@ impl CommandApi {
|
||||
&self,
|
||||
account_id: u32,
|
||||
message_id: u32,
|
||||
) -> Result<Option<JSONRPCReactions>> {
|
||||
) -> Result<Option<JsonrpcReactions>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let reactions = get_msg_reactions(&ctx, MsgId::new(message_id)).await?;
|
||||
if reactions.is_empty() {
|
||||
|
||||
@@ -32,7 +32,10 @@ impl Account {
|
||||
let addr = ctx.get_config(Config::Addr).await?;
|
||||
let profile_image = ctx.get_config(Config::Selfavatar).await?;
|
||||
let color = color_int_to_hex_string(
|
||||
Contact::get_by_id(ctx, ContactId::SELF).await?.get_color(),
|
||||
Contact::get_by_id(ctx, ContactId::SELF)
|
||||
.await?
|
||||
.get_or_gen_color(ctx)
|
||||
.await?,
|
||||
);
|
||||
let private_tag = ctx.get_config(Config::PrivateTag).await?;
|
||||
Ok(Account::Configured {
|
||||
|
||||
@@ -6,7 +6,6 @@ use deltachat::chat::{Chat, ChatId};
|
||||
use deltachat::constants::Chattype;
|
||||
use deltachat::contact::{Contact, ContactId};
|
||||
use deltachat::context::Context;
|
||||
use num_traits::cast::ToPrimitive;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
@@ -19,18 +18,6 @@ pub struct FullChat {
|
||||
id: u32,
|
||||
name: String,
|
||||
|
||||
/// True if the chat is protected.
|
||||
///
|
||||
/// Only verified contacts
|
||||
/// as determined by [`ContactObject::is_verified`] / `Contact.isVerified`
|
||||
/// can be added to protected chats.
|
||||
///
|
||||
/// Protected chats are created using [`create_group_chat`] / `createGroupChat()`
|
||||
/// by setting the 'protect' parameter to true.
|
||||
///
|
||||
/// [`create_group_chat`]: crate::api::CommandApi::create_group_chat
|
||||
is_protected: bool,
|
||||
|
||||
/// True if the chat is encrypted.
|
||||
/// This means that all messages in the chat are encrypted,
|
||||
/// and all contacts in the chat are "key-contacts",
|
||||
@@ -58,7 +45,7 @@ pub struct FullChat {
|
||||
archived: bool,
|
||||
pinned: bool,
|
||||
// subtitle - will be moved to frontend because it uses translation functions
|
||||
chat_type: u32,
|
||||
chat_type: JsonrpcChatType,
|
||||
is_unpromoted: bool,
|
||||
is_self_talk: bool,
|
||||
contacts: Vec<ContactObject>,
|
||||
@@ -73,6 +60,13 @@ pub struct FullChat {
|
||||
is_contact_request: bool,
|
||||
|
||||
is_device_chat: bool,
|
||||
/// Note that this is different from
|
||||
/// [`ChatListItem::is_self_in_group`](`crate::api::types::chat_list::ChatListItemFetchResult::ChatListItem::is_self_in_group`).
|
||||
/// This property should only be accessed
|
||||
/// when [`FullChat::chat_type`] is [`Chattype::Group`].
|
||||
//
|
||||
// We could utilize [`Chat::is_self_in_chat`],
|
||||
// but that would be an extra DB query.
|
||||
self_in_group: bool,
|
||||
is_muted: bool,
|
||||
ephemeral_timer: u32, //TODO look if there are more important properties in newer core versions
|
||||
@@ -131,12 +125,11 @@ impl FullChat {
|
||||
Ok(FullChat {
|
||||
id: chat_id,
|
||||
name: chat.name.clone(),
|
||||
is_protected: chat.is_protected(),
|
||||
is_encrypted: chat.is_encrypted(context).await?,
|
||||
profile_image, //BLOBS ?
|
||||
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
||||
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
|
||||
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
||||
chat_type: chat.get_type().into(),
|
||||
is_unpromoted: chat.is_unpromoted(),
|
||||
is_self_talk: chat.is_self_talk(),
|
||||
contacts,
|
||||
@@ -172,18 +165,6 @@ pub struct BasicChat {
|
||||
id: u32,
|
||||
name: String,
|
||||
|
||||
/// True if the chat is protected.
|
||||
///
|
||||
/// UI should display a green checkmark
|
||||
/// in the chat title,
|
||||
/// in the chat profile title and
|
||||
/// in the chatlist item
|
||||
/// if chat protection is enabled.
|
||||
/// UI should also display a green checkmark
|
||||
/// in the contact profile
|
||||
/// if 1:1 chat with this contact exists and is protected.
|
||||
is_protected: bool,
|
||||
|
||||
/// True if the chat is encrypted.
|
||||
/// This means that all messages in the chat are encrypted,
|
||||
/// and all contacts in the chat are "key-contacts",
|
||||
@@ -210,7 +191,7 @@ pub struct BasicChat {
|
||||
profile_image: Option<String>, //BLOBS ?
|
||||
archived: bool,
|
||||
pinned: bool,
|
||||
chat_type: u32,
|
||||
chat_type: JsonrpcChatType,
|
||||
is_unpromoted: bool,
|
||||
is_self_talk: bool,
|
||||
color: String,
|
||||
@@ -234,12 +215,11 @@ impl BasicChat {
|
||||
Ok(BasicChat {
|
||||
id: chat_id,
|
||||
name: chat.name.clone(),
|
||||
is_protected: chat.is_protected(),
|
||||
is_encrypted: chat.is_encrypted(context).await?,
|
||||
profile_image, //BLOBS ?
|
||||
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
||||
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
|
||||
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
||||
chat_type: chat.get_type().into(),
|
||||
is_unpromoted: chat.is_unpromoted(),
|
||||
is_self_talk: chat.is_self_talk(),
|
||||
color,
|
||||
@@ -278,18 +258,52 @@ impl MuteDuration {
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "ChatVisibility")]
|
||||
pub enum JSONRPCChatVisibility {
|
||||
pub enum JsonrpcChatVisibility {
|
||||
Normal,
|
||||
Archived,
|
||||
Pinned,
|
||||
}
|
||||
|
||||
impl JSONRPCChatVisibility {
|
||||
impl JsonrpcChatVisibility {
|
||||
pub fn into_core_type(self) -> ChatVisibility {
|
||||
match self {
|
||||
JSONRPCChatVisibility::Normal => ChatVisibility::Normal,
|
||||
JSONRPCChatVisibility::Archived => ChatVisibility::Archived,
|
||||
JSONRPCChatVisibility::Pinned => ChatVisibility::Pinned,
|
||||
JsonrpcChatVisibility::Normal => ChatVisibility::Normal,
|
||||
JsonrpcChatVisibility::Archived => ChatVisibility::Archived,
|
||||
JsonrpcChatVisibility::Pinned => ChatVisibility::Pinned,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, PartialEq, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "ChatType")]
|
||||
pub enum JsonrpcChatType {
|
||||
Single,
|
||||
Group,
|
||||
Mailinglist,
|
||||
OutBroadcast,
|
||||
InBroadcast,
|
||||
}
|
||||
|
||||
impl From<Chattype> for JsonrpcChatType {
|
||||
fn from(chattype: Chattype) -> Self {
|
||||
match chattype {
|
||||
Chattype::Single => JsonrpcChatType::Single,
|
||||
Chattype::Group => JsonrpcChatType::Group,
|
||||
Chattype::Mailinglist => JsonrpcChatType::Mailinglist,
|
||||
Chattype::OutBroadcast => JsonrpcChatType::OutBroadcast,
|
||||
Chattype::InBroadcast => JsonrpcChatType::InBroadcast,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JsonrpcChatType> for Chattype {
|
||||
fn from(chattype: JsonrpcChatType) -> Self {
|
||||
match chattype {
|
||||
JsonrpcChatType::Single => Chattype::Single,
|
||||
JsonrpcChatType::Group => Chattype::Group,
|
||||
JsonrpcChatType::Mailinglist => Chattype::Mailinglist,
|
||||
JsonrpcChatType::OutBroadcast => Chattype::OutBroadcast,
|
||||
JsonrpcChatType::InBroadcast => Chattype::InBroadcast,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use anyhow::{Context, Result};
|
||||
use deltachat::chat::{Chat, ChatId};
|
||||
use deltachat::chatlist::get_last_message_for_chat;
|
||||
use deltachat::constants::*;
|
||||
use deltachat::contact::{Contact, ContactId};
|
||||
use deltachat::contact::Contact;
|
||||
use deltachat::{
|
||||
chat::{get_chat_contacts, ChatVisibility},
|
||||
chatlist::Chatlist,
|
||||
@@ -11,6 +11,7 @@ use num_traits::cast::ToPrimitive;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
use super::chat::JsonrpcChatType;
|
||||
use super::color_int_to_hex_string;
|
||||
use super::message::MessageViewtype;
|
||||
|
||||
@@ -23,14 +24,13 @@ pub enum ChatListItemFetchResult {
|
||||
name: String,
|
||||
avatar_path: Option<String>,
|
||||
color: String,
|
||||
chat_type: u32,
|
||||
chat_type: JsonrpcChatType,
|
||||
last_updated: Option<i64>,
|
||||
summary_text1: String,
|
||||
summary_text2: String,
|
||||
summary_status: u32,
|
||||
/// showing preview if last chat message is image
|
||||
summary_preview_image: Option<String>,
|
||||
is_protected: bool,
|
||||
|
||||
/// True if the chat is encrypted.
|
||||
/// This means that all messages in the chat are encrypted,
|
||||
@@ -127,11 +127,8 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
None => (None, None),
|
||||
};
|
||||
|
||||
let chat_contacts = get_chat_contacts(ctx, chat_id).await?;
|
||||
|
||||
let self_in_group = chat_contacts.contains(&ContactId::SELF);
|
||||
|
||||
let (dm_chat_contact, was_seen_recently) = if chat.get_type() == Chattype::Single {
|
||||
let chat_contacts = get_chat_contacts(ctx, chat_id).await?;
|
||||
let contact = chat_contacts.first();
|
||||
let was_seen_recently = match contact {
|
||||
Some(contact) => Contact::get_by_id(ctx, *contact)
|
||||
@@ -155,19 +152,18 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
name: chat.get_name().to_owned(),
|
||||
avatar_path,
|
||||
color,
|
||||
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
||||
chat_type: chat.get_type().into(),
|
||||
last_updated,
|
||||
summary_text1,
|
||||
summary_text2,
|
||||
summary_status: summary.state.to_u32().expect("impossible"), // idea and a function to transform the constant to strings? or return string enum
|
||||
summary_preview_image,
|
||||
is_protected: chat.is_protected(),
|
||||
is_encrypted: chat.is_encrypted(ctx).await?,
|
||||
is_group: chat.get_type() == Chattype::Group,
|
||||
fresh_message_counter,
|
||||
is_self_talk: chat.is_self_talk(),
|
||||
is_device_talk: chat.is_device_talk(),
|
||||
is_self_in_group: self_in_group,
|
||||
is_self_in_group: chat.is_self_in_chat(ctx).await?,
|
||||
is_sending_location: chat.is_sending_locations(),
|
||||
is_archived: visibility == ChatVisibility::Archived,
|
||||
is_pinned: visibility == ChatVisibility::Pinned,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use deltachat::{Event as CoreEvent, EventType as CoreEventType};
|
||||
use num_traits::ToPrimitive;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
use super::chat::JsonrpcChatType;
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Event {
|
||||
@@ -307,7 +308,7 @@ pub enum EventType {
|
||||
/// The type of the joined chat.
|
||||
/// This can take the same values
|
||||
/// as `BasicChat.chatType` ([`crate::api::types::chat::BasicChat::chat_type`]).
|
||||
chat_type: u32,
|
||||
chat_type: JsonrpcChatType,
|
||||
/// ID of the chat in case of success.
|
||||
chat_id: u32,
|
||||
|
||||
@@ -570,7 +571,7 @@ impl From<CoreEventType> for EventType {
|
||||
progress,
|
||||
} => SecurejoinInviterProgress {
|
||||
contact_id: contact_id.to_u32(),
|
||||
chat_type: chat_type.to_u32().unwrap_or(0),
|
||||
chat_type: chat_type.into(),
|
||||
chat_id: chat_id.to_u32(),
|
||||
progress,
|
||||
},
|
||||
|
||||
@@ -16,9 +16,10 @@ use num_traits::cast::ToPrimitive;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
use super::chat::JsonrpcChatType;
|
||||
use super::color_int_to_hex_string;
|
||||
use super::contact::ContactObject;
|
||||
use super::reactions::JSONRPCReactions;
|
||||
use super::reactions::JsonrpcReactions;
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase", tag = "kind")]
|
||||
@@ -102,7 +103,7 @@ pub struct MessageObject {
|
||||
|
||||
saved_message_id: Option<u32>,
|
||||
|
||||
reactions: Option<JSONRPCReactions>,
|
||||
reactions: Option<JsonrpcReactions>,
|
||||
|
||||
vcard_contact: Option<VcardContact>,
|
||||
}
|
||||
@@ -531,8 +532,7 @@ pub struct MessageSearchResult {
|
||||
chat_profile_image: Option<String>,
|
||||
chat_color: String,
|
||||
chat_name: String,
|
||||
chat_type: u32,
|
||||
is_chat_protected: bool,
|
||||
chat_type: JsonrpcChatType,
|
||||
is_chat_contact_request: bool,
|
||||
is_chat_archived: bool,
|
||||
message: String,
|
||||
@@ -570,9 +570,8 @@ impl MessageSearchResult {
|
||||
chat_id: chat.id.to_u32(),
|
||||
chat_name: chat.get_name().to_owned(),
|
||||
chat_color,
|
||||
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
||||
chat_type: chat.get_type().into(),
|
||||
chat_profile_image,
|
||||
is_chat_protected: chat.is_protected(),
|
||||
is_chat_contact_request: chat.is_contact_request(),
|
||||
is_chat_archived: chat.get_visibility() == ChatVisibility::Archived,
|
||||
message: message.get_text(),
|
||||
@@ -583,7 +582,7 @@ impl MessageSearchResult {
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase", rename = "MessageListItem", tag = "kind")]
|
||||
pub enum JSONRPCMessageListItem {
|
||||
pub enum JsonrpcMessageListItem {
|
||||
Message {
|
||||
msg_id: u32,
|
||||
},
|
||||
@@ -596,13 +595,13 @@ pub enum JSONRPCMessageListItem {
|
||||
},
|
||||
}
|
||||
|
||||
impl From<ChatItem> for JSONRPCMessageListItem {
|
||||
impl From<ChatItem> for JsonrpcMessageListItem {
|
||||
fn from(item: ChatItem) -> Self {
|
||||
match item {
|
||||
ChatItem::Message { msg_id } => JSONRPCMessageListItem::Message {
|
||||
ChatItem::Message { msg_id } => JsonrpcMessageListItem::Message {
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
ChatItem::DayMarker { timestamp } => JSONRPCMessageListItem::DayMarker { timestamp },
|
||||
ChatItem::DayMarker { timestamp } => JsonrpcMessageListItem::DayMarker { timestamp },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ pub mod http;
|
||||
pub mod location;
|
||||
pub mod login_param;
|
||||
pub mod message;
|
||||
pub mod notify_state;
|
||||
pub mod provider_info;
|
||||
pub mod qr;
|
||||
pub mod reactions;
|
||||
|
||||
26
deltachat-jsonrpc/src/api/types/notify_state.rs
Normal file
26
deltachat-jsonrpc/src/api/types/notify_state.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use deltachat::push::NotifyState;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "NotifyState")]
|
||||
pub enum JsonrpcNotifyState {
|
||||
/// Not subscribed to push notifications.
|
||||
NotConnected,
|
||||
|
||||
/// Subscribed to heartbeat push notifications.
|
||||
Heartbeat,
|
||||
|
||||
/// Subscribed to push notifications for new messages.
|
||||
Connected,
|
||||
}
|
||||
|
||||
impl From<NotifyState> for JsonrpcNotifyState {
|
||||
fn from(state: NotifyState) -> Self {
|
||||
match state {
|
||||
NotifyState::NotConnected => Self::NotConnected,
|
||||
NotifyState::Heartbeat => Self::Heartbeat,
|
||||
NotifyState::Connected => Self::Connected,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use deltachat::qr::Qr;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
@@ -34,6 +35,26 @@ pub enum QrObject {
|
||||
/// Authentication code.
|
||||
authcode: String,
|
||||
},
|
||||
/// Ask the user whether to join the broadcast channel.
|
||||
AskJoinBroadcast {
|
||||
/// The user-visible name of this broadcast channel
|
||||
name: String,
|
||||
/// A string of random characters,
|
||||
/// uniquely identifying this broadcast channel across all databases/clients.
|
||||
/// Called `grpid` for historic reasons:
|
||||
/// The id of multi-user chats is always called `grpid` in the database
|
||||
/// because groups were once the only multi-user chats.
|
||||
grpid: String,
|
||||
/// ID of the contact who owns the broadcast channel and created the QR code.
|
||||
contact_id: u32,
|
||||
/// Fingerprint of the broadcast channel owner's key as scanned from the QR code.
|
||||
fingerprint: String,
|
||||
|
||||
/// Invite number.
|
||||
invitenumber: String,
|
||||
/// Authentication code.
|
||||
authcode: String,
|
||||
},
|
||||
/// Contact fingerprint is verified.
|
||||
///
|
||||
/// Ask the user if they want to start chatting.
|
||||
@@ -136,6 +157,21 @@ pub enum QrObject {
|
||||
/// Authentication code.
|
||||
authcode: String,
|
||||
},
|
||||
/// Ask the user if they want to withdraw their own broadcast channel invite QR code.
|
||||
WithdrawJoinBroadcast {
|
||||
/// Broadcast name.
|
||||
name: String,
|
||||
/// ID, uniquely identifying this chat. Called grpid for historic reasons.
|
||||
grpid: String,
|
||||
/// Contact ID. Always `ContactId::SELF`.
|
||||
contact_id: u32,
|
||||
/// Fingerprint of the contact key as scanned from the QR code.
|
||||
fingerprint: String,
|
||||
/// Invite number.
|
||||
invitenumber: String,
|
||||
/// Authentication code.
|
||||
authcode: String,
|
||||
},
|
||||
/// Ask the user if they want to revive their own QR code.
|
||||
ReviveVerifyContact {
|
||||
/// Contact ID.
|
||||
@@ -162,6 +198,21 @@ pub enum QrObject {
|
||||
/// Authentication code.
|
||||
authcode: String,
|
||||
},
|
||||
/// Ask the user if they want to revive their own broadcast channel invite QR code.
|
||||
ReviveJoinBroadcast {
|
||||
/// Broadcast name.
|
||||
name: String,
|
||||
/// Globally unique chat ID. Called grpid for historic reasons.
|
||||
grpid: String,
|
||||
/// Contact ID. Always `ContactId::SELF`.
|
||||
contact_id: u32,
|
||||
/// Fingerprint of the contact key as scanned from the QR code.
|
||||
fingerprint: String,
|
||||
/// Invite number.
|
||||
invitenumber: String,
|
||||
/// Authentication code.
|
||||
authcode: String,
|
||||
},
|
||||
/// `dclogin:` scheme parameters.
|
||||
///
|
||||
/// Ask the user if they want to login with the email address.
|
||||
@@ -207,6 +258,25 @@ impl From<Qr> for QrObject {
|
||||
authcode,
|
||||
}
|
||||
}
|
||||
Qr::AskJoinBroadcast {
|
||||
name,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
authcode,
|
||||
invitenumber,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::AskJoinBroadcast {
|
||||
name,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
authcode,
|
||||
invitenumber,
|
||||
}
|
||||
}
|
||||
Qr::FprOk { contact_id } => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
QrObject::FprOk { contact_id }
|
||||
@@ -266,6 +336,25 @@ impl From<Qr> for QrObject {
|
||||
authcode,
|
||||
}
|
||||
}
|
||||
Qr::WithdrawJoinBroadcast {
|
||||
name,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::WithdrawJoinBroadcast {
|
||||
name,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
}
|
||||
}
|
||||
Qr::ReviveVerifyContact {
|
||||
contact_id,
|
||||
fingerprint,
|
||||
@@ -300,7 +389,76 @@ impl From<Qr> for QrObject {
|
||||
authcode,
|
||||
}
|
||||
}
|
||||
Qr::ReviveJoinBroadcast {
|
||||
name,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::ReviveJoinBroadcast {
|
||||
name,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
}
|
||||
}
|
||||
Qr::Login { address, .. } => QrObject::Login { address },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, TypeDef, schemars::JsonSchema)]
|
||||
pub enum SecurejoinSource {
|
||||
/// Because of some problem, it is unknown where the QR code came from.
|
||||
Unknown,
|
||||
/// The user opened a link somewhere outside Delta Chat
|
||||
ExternalLink,
|
||||
/// The user clicked on a link in a message inside Delta Chat
|
||||
InternalLink,
|
||||
/// The user clicked "Paste from Clipboard" in the QR scan activity
|
||||
Clipboard,
|
||||
/// The user clicked "Load QR code as image" in the QR scan activity
|
||||
ImageLoaded,
|
||||
/// The user scanned a QR code
|
||||
Scan,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, TypeDef, schemars::JsonSchema)]
|
||||
pub enum SecurejoinUiPath {
|
||||
/// The UI path is unknown, or the user didn't open the QR code screen at all.
|
||||
Unknown,
|
||||
/// The user directly clicked on the QR icon in the main screen
|
||||
QrIcon,
|
||||
/// The user first clicked on the `+` button in the main screen,
|
||||
/// and then on "New Contact"
|
||||
NewContact,
|
||||
}
|
||||
|
||||
impl From<SecurejoinSource> for deltachat::SecurejoinSource {
|
||||
fn from(value: SecurejoinSource) -> Self {
|
||||
match value {
|
||||
SecurejoinSource::Unknown => deltachat::SecurejoinSource::Unknown,
|
||||
SecurejoinSource::ExternalLink => deltachat::SecurejoinSource::ExternalLink,
|
||||
SecurejoinSource::InternalLink => deltachat::SecurejoinSource::InternalLink,
|
||||
SecurejoinSource::Clipboard => deltachat::SecurejoinSource::Clipboard,
|
||||
SecurejoinSource::ImageLoaded => deltachat::SecurejoinSource::ImageLoaded,
|
||||
SecurejoinSource::Scan => deltachat::SecurejoinSource::Scan,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SecurejoinUiPath> for deltachat::SecurejoinUiPath {
|
||||
fn from(value: SecurejoinUiPath) -> Self {
|
||||
match value {
|
||||
SecurejoinUiPath::Unknown => deltachat::SecurejoinUiPath::Unknown,
|
||||
SecurejoinUiPath::QrIcon => deltachat::SecurejoinUiPath::QrIcon,
|
||||
SecurejoinUiPath::NewContact => deltachat::SecurejoinUiPath::NewContact,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use typescript_type_def::TypeDef;
|
||||
/// A single reaction emoji.
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "Reaction", rename_all = "camelCase")]
|
||||
pub struct JSONRPCReaction {
|
||||
pub struct JsonrpcReaction {
|
||||
/// Emoji.
|
||||
emoji: String,
|
||||
|
||||
@@ -22,14 +22,14 @@ pub struct JSONRPCReaction {
|
||||
/// Structure representing all reactions to a particular message.
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "Reactions", rename_all = "camelCase")]
|
||||
pub struct JSONRPCReactions {
|
||||
pub struct JsonrpcReactions {
|
||||
/// Map from a contact to it's reaction to message.
|
||||
reactions_by_contact: BTreeMap<u32, Vec<String>>,
|
||||
/// Unique reactions and their count, sorted in descending order.
|
||||
reactions: Vec<JSONRPCReaction>,
|
||||
reactions: Vec<JsonrpcReaction>,
|
||||
}
|
||||
|
||||
impl From<Reactions> for JSONRPCReactions {
|
||||
impl From<Reactions> for JsonrpcReactions {
|
||||
fn from(reactions: Reactions) -> Self {
|
||||
let mut reactions_by_contact: BTreeMap<u32, Vec<String>> = BTreeMap::new();
|
||||
|
||||
@@ -56,7 +56,7 @@ impl From<Reactions> for JSONRPCReactions {
|
||||
false
|
||||
};
|
||||
|
||||
let reaction = JSONRPCReaction {
|
||||
let reaction = JsonrpcReaction {
|
||||
emoji,
|
||||
count,
|
||||
is_from_self,
|
||||
@@ -64,7 +64,7 @@ impl From<Reactions> for JSONRPCReactions {
|
||||
reactions_v.push(reaction)
|
||||
}
|
||||
|
||||
JSONRPCReactions {
|
||||
JsonrpcReactions {
|
||||
reactions_by_contact,
|
||||
reactions: reactions_v,
|
||||
}
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.21.0"
|
||||
"version": "2.27.0"
|
||||
}
|
||||
|
||||
@@ -40,15 +40,35 @@ const constants = data
|
||||
key.startsWith("DC_DOWNLOAD") ||
|
||||
key.startsWith("DC_INFO_") ||
|
||||
(key.startsWith("DC_MSG") && !key.startsWith("DC_MSG_ID")) ||
|
||||
key.startsWith("DC_QR_")
|
||||
key.startsWith("DC_QR_") ||
|
||||
key.startsWith("DC_CERTCK_") ||
|
||||
key.startsWith("DC_SOCKET_") ||
|
||||
key.startsWith("DC_LP_AUTH_") ||
|
||||
key.startsWith("DC_PUSH_") ||
|
||||
key.startsWith("DC_TEXT1_") ||
|
||||
key.startsWith("DC_CHAT_TYPE")
|
||||
);
|
||||
})
|
||||
.map((row) => {
|
||||
return ` ${row.key}: ${row.value}`;
|
||||
return ` export const ${row.key} = ${row.value};`;
|
||||
})
|
||||
.join(",\n");
|
||||
.join("\n");
|
||||
|
||||
writeFileSync(
|
||||
resolve(__dirname, "../generated/constants.ts"),
|
||||
`// Generated!\n\nexport enum C {\n${constants.replace(/:/g, " =")},\n}\n`,
|
||||
`// Generated!
|
||||
|
||||
export namespace C {
|
||||
${constants}
|
||||
/** @deprecated 10-8-2025 compare string directly with \`== "Group"\` */
|
||||
export const DC_CHAT_TYPE_GROUP = "Group";
|
||||
/** @deprecated 10-8-2025 compare string directly with \`== "InBroadcast"\`*/
|
||||
export const DC_CHAT_TYPE_IN_BROADCAST = "InBroadcast";
|
||||
/** @deprecated 10-8-2025 compare string directly with \`== "Mailinglist"\` */
|
||||
export const DC_CHAT_TYPE_MAILINGLIST = "Mailinglist";
|
||||
/** @deprecated 10-8-2025 compare string directly with \`== "OutBroadcast"\` */
|
||||
export const DC_CHAT_TYPE_OUT_BROADCAST = "OutBroadcast";
|
||||
/** @deprecated 10-8-2025 compare string directly with \`== "Single"\` */
|
||||
export const DC_CHAT_TYPE_SINGLE = "Single";
|
||||
}\n`,
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "2.21.0"
|
||||
version = "2.27.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
@@ -6,9 +6,7 @@ use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{bail, ensure, Result};
|
||||
use deltachat::chat::{
|
||||
self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration, ProtectionStatus,
|
||||
};
|
||||
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration};
|
||||
use deltachat::chatlist::*;
|
||||
use deltachat::constants::*;
|
||||
use deltachat::contact::*;
|
||||
@@ -72,11 +70,6 @@ async fn reset_tables(context: &Context, bits: i32) {
|
||||
.await
|
||||
.unwrap();
|
||||
context.sql().config_cache().write().await.clear();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM leftgrps;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
println!("(8) Rest but server config reset.");
|
||||
}
|
||||
|
||||
@@ -347,7 +340,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
createchat <contact-id>\n\
|
||||
creategroup <name>\n\
|
||||
createbroadcast <name>\n\
|
||||
createprotected <name>\n\
|
||||
addmember <contact-id>\n\
|
||||
removemember <contact-id>\n\
|
||||
groupname <name>\n\
|
||||
@@ -563,7 +555,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
for i in (0..cnt).rev() {
|
||||
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)?).await?;
|
||||
println!(
|
||||
"{}#{}: {} [{} fresh] {}{}{}{}",
|
||||
"{}#{}: {} [{} fresh] {}{}{}",
|
||||
chat_prefix(&chat),
|
||||
chat.get_id(),
|
||||
chat.get_name(),
|
||||
@@ -574,7 +566,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ChatVisibility::Archived => "📦",
|
||||
ChatVisibility::Pinned => "📌",
|
||||
},
|
||||
if chat.is_protected() { "🛡️" } else { "" },
|
||||
if chat.is_contact_request() {
|
||||
"🆕"
|
||||
} else {
|
||||
@@ -689,7 +680,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
format!("{} member(s)", members.len())
|
||||
};
|
||||
println!(
|
||||
"{}#{}: {} [{}]{}{}{} {}",
|
||||
"{}#{}: {} [{}]{}{}{}",
|
||||
chat_prefix(sel_chat),
|
||||
sel_chat.get_id(),
|
||||
sel_chat.get_name(),
|
||||
@@ -707,11 +698,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
},
|
||||
_ => "".to_string(),
|
||||
},
|
||||
if sel_chat.is_protected() {
|
||||
"🛡️"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
);
|
||||
log_msglist(&context, &msglist).await?;
|
||||
if let Some(draft) = sel_chat.get_id().get_draft(&context).await? {
|
||||
@@ -740,8 +726,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
"creategroup" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <name> missing.");
|
||||
let chat_id =
|
||||
chat::create_group_chat(&context, ProtectionStatus::Unprotected, arg1).await?;
|
||||
let chat_id = chat::create_group(&context, arg1).await?;
|
||||
|
||||
println!("Group#{chat_id} created successfully.");
|
||||
}
|
||||
@@ -751,13 +736,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
|
||||
println!("Broadcast#{chat_id} created successfully.");
|
||||
}
|
||||
"createprotected" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <name> missing.");
|
||||
let chat_id =
|
||||
chat::create_group_chat(&context, ProtectionStatus::Protected, arg1).await?;
|
||||
|
||||
println!("Group#{chat_id} created and protected successfully.");
|
||||
}
|
||||
"addmember" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected");
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
@@ -1266,10 +1244,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
"providerinfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
|
||||
let proxy_enabled = context
|
||||
.get_config_bool(config::Config::ProxyEnabled)
|
||||
.await?;
|
||||
match provider::get_provider_info(&context, arg1, proxy_enabled).await {
|
||||
match provider::get_provider_info(arg1) {
|
||||
Some(info) => {
|
||||
println!("Information for provider belonging to {arg1}:");
|
||||
println!("status: {}", info.status as u32);
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "2.21.0"
|
||||
version = "2.27.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
@@ -125,6 +125,11 @@ class Account:
|
||||
"""Add a new transport."""
|
||||
yield self._rpc.add_or_update_transport.future(self.id, params)
|
||||
|
||||
@futuremethod
|
||||
def add_transport_from_qr(self, qr: str):
|
||||
"""Add a new transport using a QR code."""
|
||||
yield self._rpc.add_transport_from_qr.future(self.id, qr)
|
||||
|
||||
@futuremethod
|
||||
def list_transports(self):
|
||||
"""Return the list of all email accounts that are used as a transport in the current profile."""
|
||||
@@ -300,7 +305,7 @@ class Account:
|
||||
chats.append(AttrDict(item))
|
||||
return chats
|
||||
|
||||
def create_group(self, name: str, protect: bool = False) -> Chat:
|
||||
def create_group(self, name: str) -> Chat:
|
||||
"""Create a new group chat.
|
||||
|
||||
After creation,
|
||||
@@ -317,15 +322,11 @@ class Account:
|
||||
To check, if a chat is still unpromoted, you can look at the `is_unpromoted` property of a chat
|
||||
(see `get_full_snapshot()` / `get_basic_snapshot()`).
|
||||
This may be useful if you want to show some help for just created groups.
|
||||
|
||||
:param protect: If set to 1 the function creates group with protection initially enabled.
|
||||
Only verified members are allowed in these groups
|
||||
and end-to-end-encryption is always enabled.
|
||||
"""
|
||||
return Chat(self, self._rpc.create_group_chat(self.id, name, protect))
|
||||
return Chat(self, self._rpc.create_group_chat(self.id, name, False))
|
||||
|
||||
def create_broadcast(self, name: str) -> Chat:
|
||||
"""Create a new **broadcast channel**
|
||||
"""Create a new, outgoing **broadcast channel**
|
||||
(called "Channel" in the UI).
|
||||
|
||||
Broadcast channels are similar to groups on the sending device,
|
||||
@@ -398,9 +399,10 @@ class Account:
|
||||
next_msg_ids = self._rpc.get_next_msgs(self.id)
|
||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||
|
||||
@futuremethod
|
||||
def wait_next_messages(self) -> list[Message]:
|
||||
"""Wait for new messages and return a list of them."""
|
||||
next_msg_ids = self._rpc.wait_next_msgs(self.id)
|
||||
next_msg_ids = yield self._rpc.wait_next_msgs.future(self.id)
|
||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||
|
||||
def wait_for_incoming_msg_event(self):
|
||||
@@ -415,12 +417,21 @@ class Account:
|
||||
"""Wait for messages noticed event and return it."""
|
||||
return self.wait_for_event(EventType.MSGS_NOTICED)
|
||||
|
||||
def wait_for_msg(self, event_type) -> Message:
|
||||
"""Wait for an event about the message.
|
||||
|
||||
Consumes all events before the matching event.
|
||||
Returns a message corresponding to the msg_id field of the event.
|
||||
"""
|
||||
event = self.wait_for_event(event_type)
|
||||
return self.get_message_by_id(event.msg_id)
|
||||
|
||||
def wait_for_incoming_msg(self):
|
||||
"""Wait for incoming message and return it.
|
||||
|
||||
Consumes all events before the next incoming message event.
|
||||
"""
|
||||
return self.get_message_by_id(self.wait_for_incoming_msg_event().msg_id)
|
||||
return self.wait_for_msg(EventType.INCOMING_MSG)
|
||||
|
||||
def wait_for_securejoin_inviter_success(self):
|
||||
"""Wait until SecureJoin process finishes successfully on the inviter side."""
|
||||
|
||||
@@ -83,11 +83,10 @@ class Client:
|
||||
|
||||
def configure(self, email: str, password: str, **kwargs) -> None:
|
||||
"""Configure the client."""
|
||||
self.account.set_config("addr", email)
|
||||
self.account.set_config("mail_pw", password)
|
||||
for key, value in kwargs.items():
|
||||
self.account.set_config(key, value)
|
||||
self.account.configure()
|
||||
params = {"addr": email, "password": password}
|
||||
self.account.add_or_update_transport(params)
|
||||
self.logger.debug("Account configured")
|
||||
|
||||
def run_forever(self) -> None:
|
||||
|
||||
@@ -91,19 +91,17 @@ class ChatId(IntEnum):
|
||||
LAST_SPECIAL = 9
|
||||
|
||||
|
||||
class ChatType(IntEnum):
|
||||
class ChatType(str, Enum):
|
||||
"""Chat type."""
|
||||
|
||||
UNDEFINED = 0
|
||||
|
||||
SINGLE = 100
|
||||
SINGLE = "Single"
|
||||
"""1:1 chat, i.e. a direct chat with a single contact"""
|
||||
|
||||
GROUP = 120
|
||||
GROUP = "Group"
|
||||
|
||||
MAILINGLIST = 140
|
||||
MAILINGLIST = "Mailinglist"
|
||||
|
||||
OUT_BROADCAST = 160
|
||||
OUT_BROADCAST = "OutBroadcast"
|
||||
"""Outgoing broadcast channel, called "Channel" in the UI.
|
||||
|
||||
The user can send into this channel,
|
||||
@@ -115,7 +113,7 @@ class ChatType(IntEnum):
|
||||
which would make it hard to grep for it.
|
||||
"""
|
||||
|
||||
IN_BROADCAST = 165
|
||||
IN_BROADCAST = "InBroadcast"
|
||||
"""Incoming broadcast channel, called "Channel" in the UI.
|
||||
|
||||
This channel is read-only,
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ._utils import AttrDict
|
||||
from ._utils import AttrDict, futuremethod
|
||||
from .account import Account
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -39,6 +39,15 @@ class DeltaChat:
|
||||
"""Stop the I/O of all accounts."""
|
||||
self.rpc.stop_io_for_all_accounts()
|
||||
|
||||
@futuremethod
|
||||
def background_fetch(self, timeout_in_seconds: int) -> None:
|
||||
"""Run background fetch for all accounts."""
|
||||
yield self.rpc.background_fetch.future(timeout_in_seconds)
|
||||
|
||||
def stop_background_fetch(self) -> None:
|
||||
"""Stop ongoing background fetch."""
|
||||
self.rpc.stop_background_fetch()
|
||||
|
||||
def maybe_network(self) -> None:
|
||||
"""Indicate that the network conditions might have changed."""
|
||||
self.rpc.maybe_network()
|
||||
|
||||
@@ -93,6 +93,17 @@ class Message:
|
||||
if event.kind == EventType.MSG_DELIVERED and event.msg_id == self.id:
|
||||
break
|
||||
|
||||
def resend(self) -> None:
|
||||
"""Resend messages and make information available for newly added chat members.
|
||||
Resending sends out the original message, however, recipients and webxdc-status may differ.
|
||||
Clients that already have the original message can still ignore the resent message as
|
||||
they have tracked the state by dedicated updates.
|
||||
|
||||
Some messages cannot be resent, eg. info-messages, drafts, already pending messages,
|
||||
or messages that are not sent by SELF.
|
||||
"""
|
||||
self._rpc.resend_messages(self.account.id, [self.id])
|
||||
|
||||
@futuremethod
|
||||
def send_webxdc_realtime_advertisement(self):
|
||||
"""Send an advertisement to join the realtime channel."""
|
||||
|
||||
@@ -43,10 +43,9 @@ class ACFactory:
|
||||
@futuremethod
|
||||
def new_configured_account(self):
|
||||
"""Create a new configured account."""
|
||||
addr, password = self.get_credentials()
|
||||
account = self.get_unconfigured_account()
|
||||
params = {"addr": addr, "password": password}
|
||||
yield account.add_or_update_transport.future(params)
|
||||
domain = os.getenv("CHATMAIL_DOMAIN")
|
||||
yield account.add_transport_from_qr.future(f"dcaccount:{domain}")
|
||||
|
||||
assert account.is_configured()
|
||||
return account
|
||||
@@ -73,11 +72,11 @@ class ACFactory:
|
||||
def resetup_account(self, ac: Account) -> Account:
|
||||
"""Resetup account from scratch, losing the encryption key."""
|
||||
ac.stop_io()
|
||||
ac_clone = self.get_unconfigured_account()
|
||||
for i in ["addr", "mail_pw"]:
|
||||
ac_clone.set_config(i, ac.get_config(i))
|
||||
transports = ac.list_transports()
|
||||
ac.remove()
|
||||
ac_clone.configure()
|
||||
ac_clone = self.get_unconfigured_account()
|
||||
for transport in transports:
|
||||
ac_clone.add_or_update_transport(transport)
|
||||
return ac_clone
|
||||
|
||||
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:
|
||||
@@ -136,9 +135,15 @@ def rpc(tmp_path) -> AsyncGenerator:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def acfactory(rpc) -> AsyncGenerator:
|
||||
def dc(rpc) -> DeltaChat:
|
||||
"""Return account manager."""
|
||||
return DeltaChat(rpc)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def acfactory(dc) -> AsyncGenerator:
|
||||
"""Return account factory fixture."""
|
||||
return ACFactory(DeltaChat(rpc))
|
||||
return ACFactory(dc)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -85,11 +85,11 @@ class DirectImap:
|
||||
|
||||
def get_all_messages(self) -> list[MailMessage]:
|
||||
assert not self._idling
|
||||
return list(self.conn.fetch())
|
||||
return list(self.conn.fetch(mark_seen=False))
|
||||
|
||||
def get_unread_messages(self) -> list[str]:
|
||||
assert not self._idling
|
||||
return [msg.uid for msg in self.conn.fetch(AND(seen=False))]
|
||||
return [msg.uid for msg in self.conn.fetch(AND(seen=False), mark_seen=False)]
|
||||
|
||||
def mark_all_read(self):
|
||||
messages = self.get_unread_messages()
|
||||
@@ -173,7 +173,6 @@ class DirectImap:
|
||||
class IdleManager:
|
||||
def __init__(self, direct_imap) -> None:
|
||||
self.direct_imap = direct_imap
|
||||
self.log = direct_imap.account.log
|
||||
# fetch latest messages before starting idle so that it only
|
||||
# returns messages that arrive anew
|
||||
self.direct_imap.conn.fetch("1:*")
|
||||
@@ -181,14 +180,11 @@ class IdleManager:
|
||||
|
||||
def check(self, timeout=None) -> list[bytes]:
|
||||
"""(blocking) wait for next idle message from server."""
|
||||
self.log("imap-direct: calling idle_check")
|
||||
res = self.direct_imap.conn.idle.poll(timeout=timeout)
|
||||
self.log(f"imap-direct: idle_check returned {res!r}")
|
||||
return res
|
||||
return self.direct_imap.conn.idle.poll(timeout=timeout)
|
||||
|
||||
def wait_for_new_message(self, timeout=None) -> bytes:
|
||||
def wait_for_new_message(self) -> bytes:
|
||||
while True:
|
||||
for item in self.check(timeout=timeout):
|
||||
for item in self.check():
|
||||
if b"EXISTS" in item or b"RECENT" in item:
|
||||
return item
|
||||
|
||||
@@ -196,10 +192,8 @@ class IdleManager:
|
||||
"""Return first message with SEEN flag from a running idle-stream."""
|
||||
while True:
|
||||
for item in self.check(timeout=timeout):
|
||||
if FETCH in item:
|
||||
self.log(str(item))
|
||||
if FLAGS in item and rb"\Seen" in item:
|
||||
return int(item.split(b" ")[1])
|
||||
if FETCH in item and FLAGS in item and rb"\Seen" in item:
|
||||
return int(item.split(b" ")[1])
|
||||
|
||||
def done(self):
|
||||
"""send idle-done to server if we are currently in idle mode."""
|
||||
|
||||
@@ -9,6 +9,7 @@ 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)
|
||||
assert outgoing_call_message.get_call_info().state.kind == "Alerting"
|
||||
|
||||
@@ -67,6 +68,7 @@ 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)
|
||||
@@ -84,3 +86,24 @@ def test_ice_servers(acfactory) -> None:
|
||||
|
||||
ice_servers = alice.ice_servers()
|
||||
assert len(ice_servers) == 1
|
||||
|
||||
|
||||
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.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.MSGS_CHANGED:
|
||||
msg = bob.get_message_by_id(event.msg_id)
|
||||
if msg.get_snapshot().text == "Hello!":
|
||||
break
|
||||
|
||||
@@ -169,6 +169,8 @@ def test_imap_sync_seen_msgs(acfactory: ACFactory) -> None:
|
||||
"""
|
||||
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
|
||||
|
||||
bob.create_chat(alice)
|
||||
|
||||
alice_chat_bob.send_text("hello")
|
||||
|
||||
msg = bob.wait_for_incoming_msg()
|
||||
|
||||
538
deltachat-rpc-client/tests/test_folders.py
Normal file
538
deltachat-rpc-client/tests/test_folders.py
Normal file
@@ -0,0 +1,538 @@
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from imap_tools import AND, U
|
||||
|
||||
from deltachat_rpc_client import Contact, EventType, Message
|
||||
|
||||
|
||||
def test_move_works(acfactory):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac2.set_config("mvbox_move", "1")
|
||||
ac2.bring_online()
|
||||
|
||||
chat = ac1.create_chat(ac2)
|
||||
chat.send_text("message1")
|
||||
|
||||
# Message is moved to the movebox
|
||||
ac2.wait_for_event(EventType.IMAP_MESSAGE_MOVED)
|
||||
|
||||
# Message is downloaded
|
||||
msg = ac2.wait_for_incoming_msg().get_snapshot()
|
||||
assert msg.text == "message1"
|
||||
|
||||
|
||||
def test_move_avoids_loop(acfactory, direct_imap):
|
||||
"""Test that the message is only moved from INBOX to DeltaChat.
|
||||
|
||||
This is to avoid busy loop if moved message reappears in the Inbox
|
||||
or some scanned folder later.
|
||||
For example, this happens on servers that alias `INBOX.DeltaChat` to `DeltaChat` folder,
|
||||
so the message moved to `DeltaChat` appears as a new message in the `INBOX.DeltaChat` folder.
|
||||
We do not want to move this message from `INBOX.DeltaChat` to `DeltaChat` again.
|
||||
"""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac2.set_config("mvbox_move", "1")
|
||||
ac2.set_config("delete_server_after", "0")
|
||||
ac2.bring_online()
|
||||
|
||||
# Create INBOX.DeltaChat folder and make sure
|
||||
# it is detected by full folder scan.
|
||||
ac2_direct_imap = direct_imap(ac2)
|
||||
ac2_direct_imap.create_folder("INBOX.DeltaChat")
|
||||
ac2.stop_io()
|
||||
ac2.start_io()
|
||||
|
||||
while True:
|
||||
event = ac2.wait_for_event()
|
||||
# Wait until the end of folder scan.
|
||||
if event.kind == EventType.INFO and "Found folders:" in event.msg:
|
||||
break
|
||||
|
||||
ac1_chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
ac1_chat.send_text("Message 1")
|
||||
|
||||
# Message is moved to the DeltaChat folder and downloaded.
|
||||
ac2_msg1 = ac2.wait_for_incoming_msg().get_snapshot()
|
||||
assert ac2_msg1.text == "Message 1"
|
||||
|
||||
# Move the message to the INBOX.DeltaChat again.
|
||||
# We assume that test server uses "." as the delimiter.
|
||||
ac2_direct_imap.select_folder("DeltaChat")
|
||||
ac2_direct_imap.conn.move(["*"], "INBOX.DeltaChat")
|
||||
|
||||
ac1_chat.send_text("Message 2")
|
||||
ac2_msg2 = ac2.wait_for_incoming_msg().get_snapshot()
|
||||
assert ac2_msg2.text == "Message 2"
|
||||
|
||||
# Stop and start I/O to trigger folder scan.
|
||||
ac2.stop_io()
|
||||
ac2.start_io()
|
||||
while True:
|
||||
event = ac2.wait_for_event()
|
||||
# Wait until the end of folder scan.
|
||||
if event.kind == EventType.INFO and "Found folders:" in event.msg:
|
||||
break
|
||||
|
||||
# Check that Message 1 is still in the INBOX.DeltaChat folder
|
||||
# and Message 2 is in the DeltaChat folder.
|
||||
ac2_direct_imap.select_folder("INBOX")
|
||||
assert len(ac2_direct_imap.get_all_messages()) == 0
|
||||
ac2_direct_imap.select_folder("DeltaChat")
|
||||
assert len(ac2_direct_imap.get_all_messages()) == 1
|
||||
ac2_direct_imap.select_folder("INBOX.DeltaChat")
|
||||
assert len(ac2_direct_imap.get_all_messages()) == 1
|
||||
|
||||
|
||||
def test_reactions_for_a_reordering_move(acfactory, direct_imap):
|
||||
"""When a batch of messages is moved from Inbox to DeltaChat folder with a single MOVE command,
|
||||
their UIDs may be reordered (e.g. Gmail is known for that) which led to that messages were
|
||||
processed by receive_imf in the wrong order, and, particularly, reactions were processed before
|
||||
messages they refer to and thus dropped.
|
||||
"""
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
|
||||
addr, password = acfactory.get_credentials()
|
||||
ac2 = acfactory.get_unconfigured_account()
|
||||
ac2.add_or_update_transport({"addr": addr, "password": password})
|
||||
ac2.set_config("mvbox_move", "1")
|
||||
assert ac2.is_configured()
|
||||
|
||||
ac2.bring_online()
|
||||
chat1 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
ac2.stop_io()
|
||||
|
||||
logging.info("sending message + reaction from ac1 to ac2")
|
||||
msg1 = chat1.send_text("hi")
|
||||
msg1.wait_until_delivered()
|
||||
# It's is sad, but messages must differ in their INTERNALDATEs to be processed in the correct
|
||||
# order by DC, and most (if not all) mail servers provide only seconds precision.
|
||||
time.sleep(1.1)
|
||||
react_str = "\N{THUMBS UP SIGN}"
|
||||
msg1.send_reaction(react_str).wait_until_delivered()
|
||||
|
||||
logging.info("moving messages to ac2's DeltaChat folder in the reverse order")
|
||||
ac2_direct_imap = direct_imap(ac2)
|
||||
ac2_direct_imap.connect()
|
||||
for uid in sorted([m.uid for m in ac2_direct_imap.get_all_messages()], reverse=True):
|
||||
ac2_direct_imap.conn.move(uid, "DeltaChat")
|
||||
|
||||
logging.info("receiving messages by ac2")
|
||||
ac2.start_io()
|
||||
msg2 = Message(ac2, ac2.wait_for_reactions_changed().msg_id)
|
||||
assert msg2.get_snapshot().text == msg1.get_snapshot().text
|
||||
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.get_config("addr")
|
||||
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 "uid/validity change folder DeltaChat" 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.
|
||||
|
||||
If the draft email is sent out and received later (i.e. it's in "Inbox"), it must be shown.
|
||||
|
||||
Also, test that unknown emails in the Spam folder are not shown."""
|
||||
ac1 = acfactory.new_configured_account()
|
||||
ac1.stop_io()
|
||||
ac1.set_config("show_emails", "2")
|
||||
|
||||
ac1.create_contact("alice@example.org").create_chat()
|
||||
|
||||
ac1_direct_imap = direct_imap(ac1)
|
||||
ac1_direct_imap.create_folder("Drafts")
|
||||
ac1_direct_imap.create_folder("Spam")
|
||||
ac1_direct_imap.create_folder("Junk")
|
||||
|
||||
# Learn UID validity for all folders.
|
||||
ac1.set_config("scan_all_folders_debounce_secs", "0")
|
||||
ac1.start_io()
|
||||
ac1.wait_for_event(EventType.IMAP_INBOX_IDLE)
|
||||
ac1.stop_io()
|
||||
|
||||
ac1_direct_imap.append(
|
||||
"Drafts",
|
||||
"""
|
||||
From: ac1 <{}>
|
||||
Subject: subj
|
||||
To: alice@example.org
|
||||
Message-ID: <aepiors@example.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
message in Drafts received later
|
||||
""".format(
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
ac1_direct_imap.append(
|
||||
"Spam",
|
||||
"""
|
||||
From: unknown.address@junk.org
|
||||
Subject: subj
|
||||
To: {}
|
||||
Message-ID: <spam.message@junk.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Unknown message in Spam
|
||||
""".format(
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
ac1_direct_imap.append(
|
||||
"Spam",
|
||||
"""
|
||||
From: unknown.address@junk.org, unkwnown.add@junk.org
|
||||
Subject: subj
|
||||
To: {}
|
||||
Message-ID: <spam.message2@junk.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Unknown & malformed message in Spam
|
||||
""".format(
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
ac1_direct_imap.append(
|
||||
"Spam",
|
||||
"""
|
||||
From: delta<address: inbox@nhroy.com>
|
||||
Subject: subj
|
||||
To: {}
|
||||
Message-ID: <spam.message99@junk.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Unknown & malformed message in Spam
|
||||
""".format(
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
ac1_direct_imap.append(
|
||||
"Spam",
|
||||
"""
|
||||
From: alice@example.org
|
||||
Subject: subj
|
||||
To: {}
|
||||
Message-ID: <spam.message3@junk.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Actually interesting message in Spam
|
||||
""".format(
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
ac1_direct_imap.append(
|
||||
"Junk",
|
||||
"""
|
||||
From: unknown.address@junk.org
|
||||
Subject: subj
|
||||
To: {}
|
||||
Message-ID: <spam.message@junk.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Unknown message in Junk
|
||||
""".format(
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
|
||||
ac1.set_config("scan_all_folders_debounce_secs", "0")
|
||||
log.section("All prepared, now let DC find the message")
|
||||
ac1.start_io()
|
||||
|
||||
# Wait until each folder was scanned, this is necessary for this test to test what it should test:
|
||||
ac1.wait_for_event(EventType.IMAP_INBOX_IDLE)
|
||||
|
||||
fresh_msgs = list(ac1.get_fresh_messages())
|
||||
msg = fresh_msgs[0].get_snapshot()
|
||||
chat_msgs = msg.chat.get_messages()
|
||||
assert len(chat_msgs) == 1
|
||||
assert msg.text == "subj – Actually interesting message in Spam"
|
||||
|
||||
assert not any("unknown.address" in c.get_full_snapshot().name for c in ac1.get_chatlist())
|
||||
ac1_direct_imap.select_folder("Spam")
|
||||
assert ac1_direct_imap.get_uid_by_message_id("spam.message@junk.org")
|
||||
|
||||
ac1.stop_io()
|
||||
log.section("'Send out' the draft by moving it to Inbox, and wait for DC to display it this time")
|
||||
ac1_direct_imap.select_folder("Drafts")
|
||||
uid = ac1_direct_imap.get_uid_by_message_id("aepiors@example.org")
|
||||
ac1_direct_imap.conn.move(uid, "Inbox")
|
||||
|
||||
ac1.start_io()
|
||||
event = ac1.wait_for_event(EventType.MSGS_CHANGED)
|
||||
msg2 = Message(ac1, event.msg_id).get_snapshot()
|
||||
|
||||
assert msg2.text == "subj – message in Drafts received later"
|
||||
assert len(msg.chat.get_messages()) == 2
|
||||
|
||||
|
||||
def test_move_works_on_self_sent(acfactory):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
# Enable movebox and wait until it is created.
|
||||
ac1.set_config("mvbox_move", "1")
|
||||
ac1.set_config("bcc_self", "1")
|
||||
ac1.bring_online()
|
||||
|
||||
chat = ac1.create_chat(ac2)
|
||||
chat.send_text("message1")
|
||||
ac1.wait_for_event(EventType.IMAP_MESSAGE_MOVED)
|
||||
chat.send_text("message2")
|
||||
ac1.wait_for_event(EventType.IMAP_MESSAGE_MOVED)
|
||||
chat.send_text("message3")
|
||||
ac1.wait_for_event(EventType.IMAP_MESSAGE_MOVED)
|
||||
|
||||
|
||||
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.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.
|
||||
ac2.bring_online()
|
||||
|
||||
ac2.stop_io()
|
||||
ac2_direct_imap = direct_imap(ac2)
|
||||
with ac2_direct_imap.idle() as idle2:
|
||||
ac1.create_chat(ac2).send_text("Hello!")
|
||||
idle2.wait_for_new_message()
|
||||
|
||||
# Emulate moving of the message to DeltaChat folder by Sieve rule.
|
||||
ac2_direct_imap.conn.move(["*"], "DeltaChat")
|
||||
ac2_direct_imap.select_folder("DeltaChat")
|
||||
assert len(list(ac2_direct_imap.conn.fetch("*", mark_seen=False))) == 1
|
||||
|
||||
with ac2_direct_imap.idle() as idle2:
|
||||
ac2.start_io()
|
||||
|
||||
ev = ac2.wait_for_event(EventType.MSGS_CHANGED)
|
||||
msg = ac2.get_message_by_id(ev.msg_id)
|
||||
assert msg.get_snapshot().text == "Messages are end-to-end encrypted."
|
||||
|
||||
ev = ac2.wait_for_event(EventType.INCOMING_MSG)
|
||||
msg = ac2.get_message_by_id(ev.msg_id)
|
||||
chat = ac2.get_chat_by_id(ev.chat_id)
|
||||
|
||||
# Accept the contact request.
|
||||
chat.accept()
|
||||
msg.mark_seen()
|
||||
idle2.wait_for_seen()
|
||||
|
||||
assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True, uid=U(1, "*")), mark_seen=False))) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mvbox_move", [True, False])
|
||||
def test_markseen_message_and_mdn(acfactory, direct_imap, mvbox_move):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
for ac in ac1, ac2:
|
||||
ac.set_config("delete_server_after", "0")
|
||||
if mvbox_move:
|
||||
ac.set_config("mvbox_move", "1")
|
||||
ac.bring_online()
|
||||
|
||||
# Do not send BCC to self, we only want to test MDN on ac1.
|
||||
ac1.set_config("bcc_self", "0")
|
||||
|
||||
acfactory.get_accepted_chat(ac1, ac2).send_text("hi")
|
||||
msg = ac2.wait_for_incoming_msg()
|
||||
msg.mark_seen()
|
||||
|
||||
if mvbox_move:
|
||||
rex = re.compile("Marked messages [0-9]+ in folder DeltaChat as seen.")
|
||||
else:
|
||||
rex = re.compile("Marked messages [0-9]+ in folder INBOX as seen.")
|
||||
|
||||
for ac in ac1, ac2:
|
||||
while True:
|
||||
event = ac.wait_for_event()
|
||||
if event.kind == EventType.INFO and rex.search(event.msg):
|
||||
break
|
||||
|
||||
folder = "mvbox" if mvbox_move else "inbox"
|
||||
ac1_direct_imap = direct_imap(ac1)
|
||||
ac2_direct_imap = direct_imap(ac2)
|
||||
|
||||
ac1_direct_imap.select_config_folder(folder)
|
||||
ac2_direct_imap.select_config_folder(folder)
|
||||
|
||||
# Check that the mdn is marked as seen
|
||||
assert len(list(ac1_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 1
|
||||
# Check original message is marked as seen
|
||||
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"),
|
||||
[
|
||||
(
|
||||
"xyz",
|
||||
False,
|
||||
"xyz",
|
||||
), # Test that emails aren't found in a random folder
|
||||
(
|
||||
"xyz",
|
||||
True,
|
||||
"xyz",
|
||||
), # ...emails are found in a random folder and downloaded without moving
|
||||
(
|
||||
"Spam",
|
||||
False,
|
||||
"INBOX",
|
||||
), # ...emails are moved from the spam folder to the Inbox
|
||||
],
|
||||
)
|
||||
# Testrun.org does not support the CREATE-SPECIAL-USE capability, which means that we can't create a folder with
|
||||
# the "\Junk" flag (see https://tools.ietf.org/html/rfc6154). So, we can't test spam folder detection by flag.
|
||||
def test_scan_folders(acfactory, log, direct_imap, folder, move, expected_destination):
|
||||
"""Delta Chat periodically scans all folders for new messages to make sure we don't miss any."""
|
||||
variant = folder + "-" + str(move) + "-" + expected_destination
|
||||
log.section("Testing variant " + variant)
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("delete_server_after", "0")
|
||||
if move:
|
||||
ac1.set_config("mvbox_move", "1")
|
||||
ac1.bring_online()
|
||||
|
||||
ac1.stop_io()
|
||||
ac1_direct_imap = direct_imap(ac1)
|
||||
ac1_direct_imap.create_folder(folder)
|
||||
|
||||
# Wait until each folder was selected once and we are IDLEing:
|
||||
ac1.start_io()
|
||||
ac1.bring_online()
|
||||
|
||||
ac1.stop_io()
|
||||
assert folder in ac1_direct_imap.list_folders()
|
||||
|
||||
log.section("Send a message from ac2 to ac1 and manually move it to `folder`")
|
||||
ac1_direct_imap.select_config_folder("inbox")
|
||||
with ac1_direct_imap.idle() as idle1:
|
||||
acfactory.get_accepted_chat(ac2, ac1).send_text("hello")
|
||||
idle1.wait_for_new_message()
|
||||
ac1_direct_imap.conn.move(["*"], folder) # "*" means "biggest UID in mailbox"
|
||||
|
||||
log.section("start_io() and see if DeltaChat finds the message (" + variant + ")")
|
||||
ac1.set_config("scan_all_folders_debounce_secs", "0")
|
||||
ac1.start_io()
|
||||
chat = ac1.create_chat(ac2)
|
||||
n_msgs = 1 # "Messages are end-to-end encrypted."
|
||||
if folder == "Spam":
|
||||
msg = ac1.wait_for_incoming_msg().get_snapshot()
|
||||
assert msg.text == "hello"
|
||||
n_msgs += 1
|
||||
else:
|
||||
ac1.wait_for_event(EventType.IMAP_INBOX_IDLE)
|
||||
assert len(chat.get_messages()) == n_msgs
|
||||
|
||||
# The message has reached its destination.
|
||||
ac1_direct_imap.select_folder(expected_destination)
|
||||
assert len(ac1_direct_imap.get_all_messages()) == 1
|
||||
if folder != expected_destination:
|
||||
ac1_direct_imap.select_folder(folder)
|
||||
assert len(ac1_direct_imap.get_all_messages()) == 0
|
||||
|
||||
|
||||
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)
|
||||
|
||||
log.section("ac1: sending 3 messages")
|
||||
texts = ["first", "second", "third"]
|
||||
for text in texts:
|
||||
chat12.send_text(text)
|
||||
|
||||
log.section("ac2: waiting for all messages on the other side")
|
||||
to_delete = []
|
||||
for text in texts:
|
||||
msg = ac2.wait_for_incoming_msg().get_snapshot()
|
||||
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")
|
||||
while 1:
|
||||
ac2.wait_for_event(EventType.IMAP_MESSAGE_MOVED)
|
||||
ac2_direct_imap.select_config_folder("inbox")
|
||||
nr_msgs = len(ac2_direct_imap.get_all_messages())
|
||||
assert nr_msgs > 0
|
||||
if nr_msgs == 1:
|
||||
break
|
||||
@@ -84,7 +84,7 @@ def test_realtime_sequentially(acfactory, path_to_webxdc):
|
||||
|
||||
# share a webxdc app between ac1 and ac2
|
||||
ac1_webxdc_msg = acfactory.send_message(from_account=ac1, to_account=ac2, text="play", file=path_to_webxdc)
|
||||
ac2_webxdc_msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id)
|
||||
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
|
||||
snapshot = ac2_webxdc_msg.get_snapshot()
|
||||
assert snapshot.text == "play"
|
||||
|
||||
@@ -94,7 +94,7 @@ def test_realtime_sequentially(acfactory, path_to_webxdc):
|
||||
acfactory.send_message(from_account=ac1, to_account=ac2, text="ping1")
|
||||
|
||||
log("waiting for incoming message on ac2")
|
||||
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
snapshot = ac2.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "ping1"
|
||||
|
||||
log("sending ac2 -> ac1 realtime advertisement and additional message")
|
||||
@@ -102,7 +102,7 @@ def test_realtime_sequentially(acfactory, path_to_webxdc):
|
||||
acfactory.send_message(from_account=ac2, to_account=ac1, text="ping2")
|
||||
|
||||
log("waiting for incoming message on ac1")
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "ping2"
|
||||
|
||||
log("sending realtime data ac1 -> ac2")
|
||||
@@ -214,7 +214,9 @@ def test_advertisement_after_chatting(acfactory, path_to_webxdc):
|
||||
ac1_ac2_chat = ac1.create_chat(ac2)
|
||||
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="WebXDC", file=path_to_webxdc)
|
||||
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
|
||||
assert ac2_webxdc_msg.get_snapshot().text == "WebXDC"
|
||||
ac2_webxdc_msg_snapshot = ac2_webxdc_msg.get_snapshot()
|
||||
assert ac2_webxdc_msg_snapshot.text == "WebXDC"
|
||||
ac2_webxdc_msg_snapshot.chat.accept()
|
||||
|
||||
ac1_ac2_chat.send_text("Hello!")
|
||||
ac2_hello_msg = ac2.wait_for_incoming_msg()
|
||||
|
||||
@@ -18,9 +18,7 @@ def test_autocrypt_setup_message_key_transfer(acfactory):
|
||||
alice1 = acfactory.get_online_account()
|
||||
|
||||
alice2 = acfactory.get_unconfigured_account()
|
||||
alice2.set_config("addr", alice1.get_config("addr"))
|
||||
alice2.set_config("mail_pw", alice1.get_config("mail_pw"))
|
||||
alice2.configure()
|
||||
alice2.add_or_update_transport({"addr": alice1.get_config("addr"), "password": alice1.get_config("mail_pw")})
|
||||
alice2.bring_online()
|
||||
|
||||
setup_code = alice1.initiate_autocrypt_key_transfer()
|
||||
@@ -37,9 +35,7 @@ def test_ac_setup_message_twice(acfactory):
|
||||
alice1 = acfactory.get_online_account()
|
||||
|
||||
alice2 = acfactory.get_unconfigured_account()
|
||||
alice2.set_config("addr", alice1.get_config("addr"))
|
||||
alice2.set_config("mail_pw", alice1.get_config("mail_pw"))
|
||||
alice2.configure()
|
||||
alice2.add_or_update_transport({"addr": alice1.get_config("addr"), "password": alice1.get_config("mail_pw")})
|
||||
alice2.bring_online()
|
||||
|
||||
# Send the first Autocrypt Setup Message and ignore it.
|
||||
|
||||
@@ -4,6 +4,41 @@ from deltachat_rpc_client import EventType
|
||||
from deltachat_rpc_client.const import MessageState
|
||||
|
||||
|
||||
def test_bcc_self_delete_server_after_defaults(acfactory):
|
||||
"""Test default values for bcc_self and delete_server_after."""
|
||||
ac = acfactory.get_online_account()
|
||||
|
||||
# Initially after getting online
|
||||
# the setting bcc_self is set to 0 because there is only one device
|
||||
# and delete_server_after is "1", meaning immediate deletion.
|
||||
assert ac.get_config("bcc_self") == "0"
|
||||
assert ac.get_config("delete_server_after") == "1"
|
||||
|
||||
# Setup a second device.
|
||||
ac_clone = ac.clone()
|
||||
ac_clone.bring_online()
|
||||
|
||||
# Second device setup
|
||||
# enables bcc_self and changes default delete_server_after.
|
||||
assert ac.get_config("bcc_self") == "1"
|
||||
assert ac.get_config("delete_server_after") == "0"
|
||||
|
||||
assert ac_clone.get_config("bcc_self") == "1"
|
||||
assert ac_clone.get_config("delete_server_after") == "0"
|
||||
|
||||
# Manually disabling bcc_self
|
||||
# also restores the default for delete_server_after.
|
||||
ac.set_config("bcc_self", "0")
|
||||
assert ac.get_config("bcc_self") == "0"
|
||||
assert ac.get_config("delete_server_after") == "1"
|
||||
|
||||
# Cloning the account again enables bcc_self
|
||||
# even though it was manually disabled.
|
||||
ac_clone = ac.clone()
|
||||
assert ac.get_config("bcc_self") == "1"
|
||||
assert ac.get_config("delete_server_after") == "0"
|
||||
|
||||
|
||||
def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1_clone = ac1.clone()
|
||||
|
||||
@@ -3,6 +3,7 @@ import logging
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import Chat, EventType, SpecialContactId
|
||||
from deltachat_rpc_client.const import ChatType
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
@@ -58,8 +59,7 @@ def test_qr_setup_contact_svg(acfactory) -> None:
|
||||
assert "Alice" in svg
|
||||
|
||||
|
||||
@pytest.mark.parametrize("protect", [True, False])
|
||||
def test_qr_securejoin(acfactory, protect):
|
||||
def test_qr_securejoin(acfactory):
|
||||
alice, bob, fiona = acfactory.get_online_accounts(3)
|
||||
|
||||
# Setup second device for Alice
|
||||
@@ -67,8 +67,7 @@ def test_qr_securejoin(acfactory, protect):
|
||||
alice2 = alice.clone()
|
||||
|
||||
logging.info("Alice creates a group")
|
||||
alice_chat = alice.create_group("Group", protect=protect)
|
||||
assert alice_chat.get_basic_snapshot().is_protected == protect
|
||||
alice_chat = alice.create_group("Group")
|
||||
|
||||
logging.info("Bob joins the group")
|
||||
qr_code = alice_chat.get_qr_code()
|
||||
@@ -87,9 +86,8 @@ def test_qr_securejoin(acfactory, protect):
|
||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||
assert alice_contact_bob_snapshot.is_verified
|
||||
|
||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
snapshot = bob.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
|
||||
assert snapshot.chat.get_basic_snapshot().is_protected == protect
|
||||
|
||||
# Test that Bob verified Alice's profile.
|
||||
bob_contact_alice = bob.create_contact(alice)
|
||||
@@ -112,6 +110,143 @@ def test_qr_securejoin(acfactory, protect):
|
||||
fiona.wait_for_securejoin_joiner_success()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("all_devices_online", [True, False])
|
||||
def test_qr_securejoin_broadcast(acfactory, all_devices_online):
|
||||
alice, bob, fiona = acfactory.get_online_accounts(3)
|
||||
|
||||
alice2 = alice.clone()
|
||||
bob2 = bob.clone()
|
||||
|
||||
if all_devices_online:
|
||||
alice2.start_io()
|
||||
bob2.start_io()
|
||||
|
||||
logging.info("===================== Alice creates a broadcast =====================")
|
||||
alice_chat = alice.create_broadcast("Broadcast channel!")
|
||||
snapshot = alice_chat.get_basic_snapshot()
|
||||
assert not snapshot.is_unpromoted # Broadcast channels are never unpromoted
|
||||
|
||||
logging.info("===================== Bob joins the broadcast =====================")
|
||||
|
||||
qr_code = alice_chat.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
alice_chat.send_text("Hello everyone!")
|
||||
|
||||
def get_broadcast(ac):
|
||||
chat = ac.get_chatlist(query="Broadcast channel!")[0]
|
||||
assert chat.get_basic_snapshot().name == "Broadcast channel!"
|
||||
return chat
|
||||
|
||||
def wait_for_broadcast_messages(ac):
|
||||
snapshot1 = ac.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot1.text == "You joined the channel."
|
||||
|
||||
snapshot2 = ac.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot2.text == "Hello everyone!"
|
||||
|
||||
chat = get_broadcast(ac)
|
||||
assert snapshot1.chat_id == chat.id
|
||||
assert snapshot2.chat_id == chat.id
|
||||
|
||||
def check_account(ac, contact, inviter_side, please_wait_info_msg=False):
|
||||
# Check that the chat partner is verified.
|
||||
contact_snapshot = contact.get_snapshot()
|
||||
assert contact_snapshot.is_verified
|
||||
|
||||
chat = get_broadcast(ac)
|
||||
chat_msgs = chat.get_messages()
|
||||
|
||||
if please_wait_info_msg:
|
||||
first_msg = chat_msgs.pop(0).get_snapshot()
|
||||
assert first_msg.text == "Establishing guaranteed end-to-end encryption, please wait…"
|
||||
assert first_msg.is_info
|
||||
|
||||
encrypted_msg = chat_msgs[0].get_snapshot()
|
||||
assert encrypted_msg.text == "Messages are end-to-end encrypted."
|
||||
assert encrypted_msg.is_info
|
||||
|
||||
member_added_msg = chat_msgs[1].get_snapshot()
|
||||
if inviter_side:
|
||||
assert member_added_msg.text == f"Member {contact_snapshot.display_name} added."
|
||||
else:
|
||||
assert member_added_msg.text == "You joined the channel."
|
||||
assert member_added_msg.is_info
|
||||
|
||||
hello_msg = chat_msgs[2].get_snapshot()
|
||||
assert hello_msg.text == "Hello everyone!"
|
||||
assert not hello_msg.is_info
|
||||
assert hello_msg.show_padlock
|
||||
assert hello_msg.error is None
|
||||
|
||||
assert len(chat_msgs) == 3
|
||||
|
||||
chat_snapshot = chat.get_full_snapshot()
|
||||
assert chat_snapshot.is_encrypted
|
||||
assert chat_snapshot.name == "Broadcast channel!"
|
||||
if inviter_side:
|
||||
assert chat_snapshot.chat_type == ChatType.OUT_BROADCAST
|
||||
else:
|
||||
assert chat_snapshot.chat_type == ChatType.IN_BROADCAST
|
||||
assert chat_snapshot.can_send == inviter_side
|
||||
|
||||
chat_contacts = chat_snapshot.contact_ids
|
||||
assert contact.id in chat_contacts
|
||||
if inviter_side:
|
||||
assert len(chat_contacts) == 1
|
||||
else:
|
||||
assert len(chat_contacts) == 2
|
||||
assert SpecialContactId.SELF in chat_contacts
|
||||
assert chat_snapshot.self_in_group
|
||||
|
||||
wait_for_broadcast_messages(bob)
|
||||
|
||||
check_account(alice, alice.create_contact(bob), inviter_side=True)
|
||||
check_account(bob, bob.create_contact(alice), inviter_side=False, please_wait_info_msg=True)
|
||||
|
||||
logging.info("===================== Test Alice's second device =====================")
|
||||
|
||||
# Start second Alice device, if it wasn't started already.
|
||||
alice2.start_io()
|
||||
|
||||
while True:
|
||||
msg_id = alice2.wait_for_msgs_changed_event().msg_id
|
||||
if msg_id:
|
||||
snapshot = alice2.get_message_by_id(msg_id).get_snapshot()
|
||||
if snapshot.text == "Hello everyone!":
|
||||
break
|
||||
|
||||
check_account(alice2, alice2.create_contact(bob), inviter_side=True)
|
||||
|
||||
logging.info("===================== Test Bob's second device =====================")
|
||||
|
||||
# Start second Bob device, if it wasn't started already.
|
||||
bob2.start_io()
|
||||
bob2.wait_for_securejoin_joiner_success()
|
||||
wait_for_broadcast_messages(bob2)
|
||||
check_account(bob2, bob2.create_contact(alice), inviter_side=False)
|
||||
|
||||
# The QR code token is synced, so alice2 must be able to handle join requests.
|
||||
logging.info("===================== Fiona joins the group via alice2 =====================")
|
||||
alice.stop_io()
|
||||
fiona.secure_join(qr_code)
|
||||
alice2.wait_for_securejoin_inviter_success()
|
||||
fiona.wait_for_securejoin_joiner_success()
|
||||
|
||||
snapshot = fiona.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "You joined the channel."
|
||||
|
||||
get_broadcast(alice2).get_messages()[2].resend()
|
||||
snapshot = fiona.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "Hello everyone!"
|
||||
|
||||
check_account(fiona, fiona.create_contact(alice), inviter_side=False, please_wait_info_msg=True)
|
||||
|
||||
# For Bob, the channel must not have changed:
|
||||
check_account(bob, bob.create_contact(alice), inviter_side=False, please_wait_info_msg=True)
|
||||
|
||||
|
||||
def test_qr_securejoin_contact_request(acfactory) -> None:
|
||||
"""Alice invites Bob to a group when Bob's chat with Alice is in a contact request mode."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
@@ -120,13 +255,13 @@ def test_qr_securejoin_contact_request(acfactory) -> None:
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
snapshot = bob.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "Hello!"
|
||||
bob_chat_alice = snapshot.chat
|
||||
assert bob_chat_alice.get_basic_snapshot().is_contact_request
|
||||
|
||||
alice_chat = alice.create_group("Verified group", protect=True)
|
||||
logging.info("Bob joins verified group")
|
||||
alice_chat = alice.create_group("Group")
|
||||
logging.info("Bob joins the group")
|
||||
qr_code = alice_chat.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
while True:
|
||||
@@ -150,8 +285,8 @@ def test_qr_readreceipt(acfactory) -> None:
|
||||
for joiner in [bob, charlie]:
|
||||
joiner.wait_for_securejoin_joiner_success()
|
||||
|
||||
logging.info("Alice creates a verified group")
|
||||
group = alice.create_group("Group", protect=True)
|
||||
logging.info("Alice creates a group")
|
||||
group = alice.create_group("Group")
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_contact_charlie = alice.create_contact(charlie, "Charlie")
|
||||
@@ -164,8 +299,7 @@ def test_qr_readreceipt(acfactory) -> None:
|
||||
|
||||
logging.info("Bob and Charlie receive a group")
|
||||
|
||||
bob_msg_id = bob.wait_for_incoming_msg_event().msg_id
|
||||
bob_message = bob.get_message_by_id(bob_msg_id)
|
||||
bob_message = bob.wait_for_incoming_msg()
|
||||
bob_snapshot = bob_message.get_snapshot()
|
||||
assert bob_snapshot.text == "Hello"
|
||||
|
||||
@@ -176,8 +310,7 @@ def test_qr_readreceipt(acfactory) -> None:
|
||||
|
||||
bob_out_message = bob_snapshot.chat.send_message(text="Hi from Bob!")
|
||||
|
||||
charlie_msg_id = charlie.wait_for_incoming_msg_event().msg_id
|
||||
charlie_message = charlie.get_message_by_id(charlie_msg_id)
|
||||
charlie_message = charlie.wait_for_incoming_msg()
|
||||
charlie_snapshot = charlie_message.get_snapshot()
|
||||
assert charlie_snapshot.text == "Hi from Bob!"
|
||||
|
||||
@@ -216,11 +349,10 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
"""Tests verified group recovery by reverifying then removing and adding a member back."""
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
|
||||
logging.info("ac1 creates verified group")
|
||||
chat = ac1.create_group("Verified group", protect=True)
|
||||
assert chat.get_basic_snapshot().is_protected
|
||||
logging.info("ac1 creates a group")
|
||||
chat = ac1.create_group("Group")
|
||||
|
||||
logging.info("ac2 joins verified group")
|
||||
logging.info("ac2 joins the group")
|
||||
qr_code = chat.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
@@ -253,7 +385,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
ac3_contact_ac2 = ac3.create_contact(ac2)
|
||||
ac3_chat.remove_contact(ac3_contact_ac2_old)
|
||||
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
|
||||
assert "removed" in snapshot.text
|
||||
|
||||
ac3_chat.add_contact(ac3_contact_ac2)
|
||||
@@ -266,25 +398,26 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
logging.info("ac2 got event message: %s", snapshot.text)
|
||||
assert "added" in snapshot.text
|
||||
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
|
||||
assert "added" in snapshot.text
|
||||
|
||||
chat = Chat(ac2, chat_id)
|
||||
chat.send_text("Works again!")
|
||||
|
||||
msg_id = ac3.wait_for_incoming_msg_event().msg_id
|
||||
message = ac3.get_message_by_id(msg_id)
|
||||
message = ac3.wait_for_incoming_msg()
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.text == "Works again!"
|
||||
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "Works again!"
|
||||
|
||||
ac1_contact_ac2 = ac1.create_contact(ac2)
|
||||
ac1_contact_ac3 = ac1.create_contact(ac3)
|
||||
ac1_contact_ac2_snapshot = ac1_contact_ac2.get_snapshot()
|
||||
assert ac1_contact_ac2_snapshot.is_verified
|
||||
assert ac1_contact_ac2_snapshot.verifier_id == ac1_contact_ac3.id
|
||||
# Until we reset verifications and then send the _verified header,
|
||||
# verification is not gossiped here:
|
||||
assert not ac1_contact_ac2_snapshot.is_verified
|
||||
assert ac1_contact_ac2_snapshot.verifier_id != ac1_contact_ac3.id
|
||||
|
||||
|
||||
def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||
@@ -302,8 +435,8 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||
# we first create a fully joined verified group, and then start
|
||||
# joining a second time but interrupt it, to create pending bob state
|
||||
|
||||
logging.info("ac1: create verified group that ac2 fully joins")
|
||||
ch1 = ac1.create_group("Group", protect=True)
|
||||
logging.info("ac1: create a group that ac2 fully joins")
|
||||
ch1 = ac1.create_group("Group")
|
||||
qr_code = ch1.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
@@ -311,9 +444,8 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||
# ensure ac1 can write and ac2 receives messages in verified chat
|
||||
ch1.send_text("ac1 says hello")
|
||||
while 1:
|
||||
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
snapshot = ac2.wait_for_incoming_msg().get_snapshot()
|
||||
if snapshot.text == "ac1 says hello":
|
||||
assert snapshot.chat.get_basic_snapshot().is_protected
|
||||
break
|
||||
|
||||
logging.info("ac1: let ac2 join again but shutoff ac1 in the middle of securejoin")
|
||||
@@ -327,15 +459,14 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||
assert ac2.create_contact(ac3).get_snapshot().is_verified
|
||||
|
||||
logging.info("ac3: create a verified group VG with ac2")
|
||||
vg = ac3.create_group("ac3-created", protect=True)
|
||||
vg = ac3.create_group("ac3-created")
|
||||
vg.add_contact(ac3.create_contact(ac2))
|
||||
|
||||
# ensure ac2 receives message in VG
|
||||
vg.send_text("hello")
|
||||
while 1:
|
||||
msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
msg = ac2.wait_for_incoming_msg().get_snapshot()
|
||||
if msg.text == "hello":
|
||||
assert msg.chat.get_basic_snapshot().is_protected
|
||||
break
|
||||
|
||||
logging.info("ac3: create a join-code for group VG and let ac4 join, check that ac2 got it")
|
||||
@@ -359,7 +490,7 @@ def test_qr_new_group_unblocked(acfactory):
|
||||
"""
|
||||
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1_chat = ac1.create_group("Group for joining", protect=True)
|
||||
ac1_chat = ac1.create_group("Group for joining")
|
||||
qr_code = ac1_chat.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
|
||||
@@ -371,7 +502,7 @@ def test_qr_new_group_unblocked(acfactory):
|
||||
ac2.wait_for_incoming_msg_event()
|
||||
|
||||
ac1_new_chat.send_text("Hello!")
|
||||
ac2_msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
ac2_msg = ac2.wait_for_incoming_msg().get_snapshot()
|
||||
assert ac2_msg.text == "Hello!"
|
||||
assert ac2_msg.chat.get_basic_snapshot().is_contact_request
|
||||
|
||||
@@ -384,8 +515,7 @@ def test_aeap_flow_verified(acfactory):
|
||||
addr, password = acfactory.get_credentials()
|
||||
|
||||
logging.info("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat = ac1.create_group("hello", protect=True)
|
||||
assert chat.get_basic_snapshot().is_protected
|
||||
chat = ac1.create_group("hello")
|
||||
qr_code = chat.get_qr_code()
|
||||
logging.info("ac2: start QR-code based join-group protocol")
|
||||
ac2.secure_join(qr_code)
|
||||
@@ -397,7 +527,7 @@ def test_aeap_flow_verified(acfactory):
|
||||
|
||||
logging.info("receiving first message")
|
||||
ac2.wait_for_incoming_msg_event() # member added message
|
||||
msg_in_1 = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
msg_in_1 = ac2.wait_for_incoming_msg().get_snapshot()
|
||||
assert msg_in_1.text == msg_out.text
|
||||
|
||||
logging.info("changing email account")
|
||||
@@ -411,7 +541,7 @@ def test_aeap_flow_verified(acfactory):
|
||||
msg_out = chat.send_text("changed address").get_snapshot()
|
||||
|
||||
logging.info("receiving second message")
|
||||
msg_in_2 = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id)
|
||||
msg_in_2 = ac2.wait_for_incoming_msg()
|
||||
msg_in_2_snapshot = msg_in_2.get_snapshot()
|
||||
assert msg_in_2_snapshot.text == msg_out.text
|
||||
assert msg_in_2_snapshot.chat.id == msg_in_1.chat.id
|
||||
@@ -439,33 +569,35 @@ def test_gossip_verification(acfactory) -> None:
|
||||
|
||||
logging.info("Bob creates an Autocrypt group")
|
||||
bob_group_chat = bob.create_group("Autocrypt Group")
|
||||
assert not bob_group_chat.get_basic_snapshot().is_protected
|
||||
bob_group_chat.add_contact(bob_contact_alice)
|
||||
bob_group_chat.add_contact(bob_contact_carol)
|
||||
bob_group_chat.send_message(text="Hello Autocrypt group")
|
||||
|
||||
snapshot = carol.get_message_by_id(carol.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
snapshot = carol.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "Hello Autocrypt group"
|
||||
assert snapshot.show_padlock
|
||||
|
||||
# Autocrypt group does not propagate verification.
|
||||
# Group propagates verification using Autocrypt-Gossip header.
|
||||
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
|
||||
# Until we reset verifications and then send the _verified header,
|
||||
# verification is not gossiped here:
|
||||
assert not carol_contact_alice_snapshot.is_verified
|
||||
|
||||
logging.info("Bob creates a Securejoin group")
|
||||
bob_group_chat = bob.create_group("Securejoin Group", protect=True)
|
||||
assert bob_group_chat.get_basic_snapshot().is_protected
|
||||
bob_group_chat = bob.create_group("Securejoin Group")
|
||||
bob_group_chat.add_contact(bob_contact_alice)
|
||||
bob_group_chat.add_contact(bob_contact_carol)
|
||||
bob_group_chat.send_message(text="Hello Securejoin group")
|
||||
|
||||
snapshot = carol.get_message_by_id(carol.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
snapshot = carol.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "Hello Securejoin group"
|
||||
assert snapshot.show_padlock
|
||||
|
||||
# Securejoin propagates verification.
|
||||
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
|
||||
assert carol_contact_alice_snapshot.is_verified
|
||||
# Until we reset verifications and then send the _verified header,
|
||||
# verification is not gossiped here:
|
||||
assert not carol_contact_alice_snapshot.is_verified
|
||||
|
||||
|
||||
def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||
@@ -477,7 +609,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
|
||||
# ac3 creates protected group with ac1.
|
||||
ac3_chat = ac3.create_group("Verified group", protect=True)
|
||||
ac3_chat = ac3.create_group("Group")
|
||||
|
||||
# ac1 joins ac3 group.
|
||||
ac3_qr_code = ac3_chat.get_qr_code()
|
||||
@@ -485,7 +617,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||
ac1.wait_for_securejoin_joiner_success()
|
||||
|
||||
# ac1 waits for member added message and creates a QR code.
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "Member Me added by {}.".format(ac3.get_config("addr"))
|
||||
ac1_qr_code = snapshot.chat.get_qr_code()
|
||||
|
||||
@@ -522,10 +654,9 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||
|
||||
# Wait for member added.
|
||||
logging.info("ac2 waits for member added message")
|
||||
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
snapshot = ac2.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.is_info
|
||||
ac2_chat = snapshot.chat
|
||||
assert ac2_chat.get_basic_snapshot().is_protected
|
||||
assert len(ac2_chat.get_contacts()) == 3
|
||||
|
||||
# ac1 is still "not verified" for ac2 due to inconsistent state.
|
||||
@@ -535,9 +666,8 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||
def test_withdraw_securejoin_qr(acfactory):
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
logging.info("Alice creates a verified group")
|
||||
alice_chat = alice.create_group("Verified group", protect=True)
|
||||
assert alice_chat.get_basic_snapshot().is_protected
|
||||
logging.info("Alice creates a group")
|
||||
alice_chat = alice.create_group("Group")
|
||||
logging.info("Bob joins verified group")
|
||||
|
||||
qr_code = alice_chat.get_qr_code()
|
||||
@@ -546,9 +676,8 @@ def test_withdraw_securejoin_qr(acfactory):
|
||||
|
||||
alice.clear_all_events()
|
||||
|
||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
snapshot = bob.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
|
||||
assert snapshot.chat.get_basic_snapshot().is_protected
|
||||
bob_chat.leave()
|
||||
|
||||
snapshot = alice.get_message_by_id(alice.wait_for_msgs_changed_event().msg_id).get_snapshot()
|
||||
|
||||
@@ -11,7 +11,7 @@ from unittest.mock import MagicMock
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import Contact, EventType, Message, events
|
||||
from deltachat_rpc_client.const import ChatType, DownloadState, MessageState
|
||||
from deltachat_rpc_client.const import DownloadState, MessageState
|
||||
from deltachat_rpc_client.pytestplugin import E2EE_INFO_MSGS
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
@@ -338,44 +338,26 @@ def test_receive_imf_failure(acfactory) -> None:
|
||||
|
||||
bob.set_config("fail_on_receiving_full_msg", "1")
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
chat_id = event.chat_id
|
||||
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()
|
||||
assert snapshot.chat_id == chat_id
|
||||
assert snapshot.download_state == DownloadState.AVAILABLE
|
||||
assert snapshot.error is not None
|
||||
assert snapshot.show_padlock
|
||||
assert (
|
||||
snapshot.text == "❌ Failed to receive a message:"
|
||||
" Condition failed: `!context.get_config_bool(Config::FailOnReceivingFullMsg).await?`."
|
||||
" 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")
|
||||
alice_chat_bob.send_text("Hello again!")
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
assert event.chat_id == chat_id
|
||||
msg_id = event.msg_id
|
||||
message1 = bob.get_message_by_id(msg_id)
|
||||
snapshot = message1.get_snapshot()
|
||||
assert snapshot.chat_id == chat_id
|
||||
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
|
||||
|
||||
# The failed message can be re-downloaded later.
|
||||
bob._rpc.download_full_message(bob.id, message.id)
|
||||
event = bob.wait_for_event(EventType.MSGS_CHANGED)
|
||||
message = bob.get_message_by_id(event.msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.download_state == DownloadState.IN_PROGRESS
|
||||
event = bob.wait_for_event(EventType.MSGS_CHANGED)
|
||||
assert event.chat_id == chat_id
|
||||
msg_id = event.msg_id
|
||||
message = bob.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.chat_id == chat_id
|
||||
assert snapshot.download_state == DownloadState.DONE
|
||||
assert snapshot.error is None
|
||||
assert snapshot.text == "Hello!"
|
||||
|
||||
|
||||
def test_selfavatar_sync(acfactory, data, log) -> None:
|
||||
alice = acfactory.get_online_account()
|
||||
@@ -439,10 +421,7 @@ def test_is_bot(acfactory) -> None:
|
||||
alice.set_config("bot", "1")
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
message = bob.get_message_by_id(event.msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.chat_id == event.chat_id
|
||||
snapshot = bob.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "Hello!"
|
||||
assert snapshot.is_bot
|
||||
|
||||
@@ -500,22 +479,21 @@ def test_wait_next_messages(acfactory) -> None:
|
||||
# There are no old messages and the call returns immediately.
|
||||
assert not bot.wait_next_messages()
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||
# Bot starts waiting for messages.
|
||||
next_messages_task = executor.submit(bot.wait_next_messages)
|
||||
# Bot starts waiting for messages.
|
||||
next_messages_task = bot.wait_next_messages.future()
|
||||
|
||||
alice_contact_bot = alice.create_contact(bot, "Bot")
|
||||
alice_chat_bot = alice_contact_bot.create_chat()
|
||||
alice_chat_bot.send_text("Hello!")
|
||||
alice_contact_bot = alice.create_contact(bot, "Bot")
|
||||
alice_chat_bot = alice_contact_bot.create_chat()
|
||||
alice_chat_bot.send_text("Hello!")
|
||||
|
||||
next_messages = next_messages_task.result()
|
||||
next_messages = next_messages_task()
|
||||
|
||||
if len(next_messages) == E2EE_INFO_MSGS:
|
||||
next_messages += bot.wait_next_messages()
|
||||
if len(next_messages) == E2EE_INFO_MSGS:
|
||||
next_messages += bot.wait_next_messages()
|
||||
|
||||
assert len(next_messages) == 1 + E2EE_INFO_MSGS
|
||||
snapshot = next_messages[0 + E2EE_INFO_MSGS].get_snapshot()
|
||||
assert snapshot.text == "Hello!"
|
||||
assert len(next_messages) == 1 + E2EE_INFO_MSGS
|
||||
snapshot = next_messages[0 + E2EE_INFO_MSGS].get_snapshot()
|
||||
assert snapshot.text == "Hello!"
|
||||
|
||||
|
||||
def test_import_export_backup(acfactory, tmp_path) -> None:
|
||||
@@ -535,7 +513,7 @@ def test_import_export_keys(acfactory, tmp_path) -> None:
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
alice_chat_bob.send_text("Hello Bob!")
|
||||
|
||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
snapshot = bob.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "Hello Bob!"
|
||||
|
||||
# Alice resetups account, but keeps the key.
|
||||
@@ -547,7 +525,7 @@ def test_import_export_keys(acfactory, tmp_path) -> None:
|
||||
|
||||
snapshot.chat.accept()
|
||||
snapshot.chat.send_text("Hello Alice!")
|
||||
snapshot = alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
snapshot = alice.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "Hello Alice!"
|
||||
assert snapshot.show_padlock
|
||||
|
||||
@@ -570,8 +548,11 @@ def test_provider_info(rpc) -> None:
|
||||
assert provider_info is None
|
||||
|
||||
# Test MX record resolution.
|
||||
# This previously resulted in Gmail provider
|
||||
# because MX record pointed to google.com domain,
|
||||
# but MX record resolution has been removed.
|
||||
provider_info = rpc.get_provider_info(account_id, "github.com")
|
||||
assert provider_info["id"] == "gmail"
|
||||
assert provider_info is None
|
||||
|
||||
# Disable MX record resolution.
|
||||
rpc.set_config(account_id, "proxy_enabled", "1")
|
||||
@@ -589,18 +570,13 @@ def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
|
||||
|
||||
# Alice sends a message to Bob.
|
||||
alice_chat_bob.send_text("Hello Bob!")
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
msg_id = event.msg_id
|
||||
message = bob.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
snapshot = bob.wait_for_incoming_msg().get_snapshot()
|
||||
|
||||
# Bob sends a message to Alice.
|
||||
bob_chat_alice = snapshot.chat
|
||||
bob_chat_alice.accept()
|
||||
bob_chat_alice.send_text("Hello Alice!")
|
||||
event = alice.wait_for_incoming_msg_event()
|
||||
msg_id = event.msg_id
|
||||
message = alice.get_message_by_id(msg_id)
|
||||
message = alice.wait_for_incoming_msg()
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.show_padlock
|
||||
|
||||
@@ -610,10 +586,7 @@ def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
|
||||
|
||||
# Bob sends a message to Alice, it should also be encrypted.
|
||||
bob_chat_alice.send_text("Hi Alice!")
|
||||
event = alice.wait_for_incoming_msg_event()
|
||||
msg_id = event.msg_id
|
||||
message = alice.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
snapshot = alice.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.show_padlock
|
||||
|
||||
|
||||
@@ -671,50 +644,6 @@ def test_reaction_to_partially_fetched_msg(acfactory, tmp_path):
|
||||
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
|
||||
|
||||
|
||||
def test_reactions_for_a_reordering_move(acfactory, direct_imap):
|
||||
"""When a batch of messages is moved from Inbox to DeltaChat folder with a single MOVE command,
|
||||
their UIDs may be reordered (e.g. Gmail is known for that) which led to that messages were
|
||||
processed by receive_imf in the wrong order, and, particularly, reactions were processed before
|
||||
messages they refer to and thus dropped.
|
||||
"""
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
|
||||
addr, password = acfactory.get_credentials()
|
||||
ac2 = acfactory.get_unconfigured_account()
|
||||
ac2.add_or_update_transport({"addr": addr, "password": password})
|
||||
ac2.set_config("mvbox_move", "1")
|
||||
assert ac2.is_configured()
|
||||
|
||||
ac2.bring_online()
|
||||
chat1 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
ac2.stop_io()
|
||||
|
||||
logging.info("sending message + reaction from ac1 to ac2")
|
||||
msg1 = chat1.send_text("hi")
|
||||
msg1.wait_until_delivered()
|
||||
# It's is sad, but messages must differ in their INTERNALDATEs to be processed in the correct
|
||||
# order by DC, and most (if not all) mail servers provide only seconds precision.
|
||||
time.sleep(1.1)
|
||||
react_str = "\N{THUMBS UP SIGN}"
|
||||
msg1.send_reaction(react_str).wait_until_delivered()
|
||||
|
||||
logging.info("moving messages to ac2's DeltaChat folder in the reverse order")
|
||||
ac2_direct_imap = direct_imap(ac2)
|
||||
ac2_direct_imap.connect()
|
||||
for uid in sorted([m.uid for m in ac2_direct_imap.get_all_messages()], reverse=True):
|
||||
ac2_direct_imap.conn.move(uid, "DeltaChat")
|
||||
|
||||
logging.info("receiving messages by ac2")
|
||||
ac2.start_io()
|
||||
msg2 = Message(ac2, ac2.wait_for_reactions_changed().msg_id)
|
||||
assert msg2.get_snapshot().text == msg1.get_snapshot().text
|
||||
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.get_config("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
|
||||
@@ -726,7 +655,7 @@ def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
|
||||
for account in others:
|
||||
chat = account.create_chat(alice)
|
||||
chat.send_text("Hello Alice!")
|
||||
assert alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot().text == "Hello Alice!"
|
||||
assert alice.wait_for_incoming_msg().get_snapshot().text == "Hello Alice!"
|
||||
|
||||
contact = alice.create_contact(account)
|
||||
alice_group.add_contact(contact)
|
||||
@@ -736,7 +665,7 @@ def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
|
||||
bob.set_config("download_limit", str(download_limit))
|
||||
|
||||
alice_group.send_text("hi")
|
||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
snapshot = bob.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "hi"
|
||||
bob_group = snapshot.chat
|
||||
|
||||
@@ -746,7 +675,7 @@ def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
|
||||
for i in range(10):
|
||||
logging.info("Sending message %s", i)
|
||||
alice_group.send_file(str(path))
|
||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
snapshot = bob.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.download_state == DownloadState.AVAILABLE
|
||||
if n_accounts > 2:
|
||||
assert snapshot.chat == bob_group
|
||||
@@ -773,8 +702,8 @@ def test_markseen_contact_request(acfactory):
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
alice_chat_bob.send_text("Hello Bob!")
|
||||
|
||||
message = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id)
|
||||
message2 = bob2.get_message_by_id(bob2.wait_for_incoming_msg_event().msg_id)
|
||||
message = bob.wait_for_incoming_msg()
|
||||
message2 = bob2.wait_for_incoming_msg()
|
||||
assert message2.get_snapshot().state == MessageState.IN_FRESH
|
||||
|
||||
message.mark_seen()
|
||||
@@ -796,7 +725,7 @@ def test_read_receipt(acfactory):
|
||||
msg = bob.wait_for_incoming_msg()
|
||||
msg.mark_seen()
|
||||
|
||||
read_msg = alice.get_message_by_id(alice.wait_for_event(EventType.MSG_READ).msg_id)
|
||||
read_msg = alice.wait_for_msg(EventType.MSG_READ)
|
||||
read_receipts = read_msg.get_read_receipts()
|
||||
assert len(read_receipts) == 1
|
||||
assert read_receipts[0].contact_id == alice_contact_bob.id
|
||||
@@ -813,7 +742,7 @@ def test_configured_imap_certificate_checks(acfactory):
|
||||
alice = acfactory.new_configured_account()
|
||||
|
||||
# Certificate checks should be configured (not None)
|
||||
assert "cert_automatic" in alice.get_info().used_account_settings
|
||||
assert "cert_strict" in alice.get_info().used_account_settings
|
||||
|
||||
# "cert_old_automatic" is the value old Delta Chat core versions used
|
||||
# to mean user entered "imap_certificate_checks=0" (Automatic)
|
||||
@@ -885,10 +814,12 @@ def test_rename_group(acfactory):
|
||||
bob_msg = bob.wait_for_incoming_msg()
|
||||
bob_chat = bob_msg.get_snapshot().chat
|
||||
assert bob_chat.get_basic_snapshot().name == "Test group"
|
||||
bob.wait_for_event(EventType.CHATLIST_ITEM_CHANGED)
|
||||
|
||||
for name in ["Baz", "Foo bar", "Xyzzy"]:
|
||||
alice_group.set_name(name)
|
||||
bob.wait_for_incoming_msg_event()
|
||||
bob.wait_for_event(EventType.CHATLIST_ITEM_CHANGED)
|
||||
bob.wait_for_event(EventType.CHATLIST_ITEM_CHANGED)
|
||||
assert bob_chat.get_basic_snapshot().name == name
|
||||
|
||||
|
||||
@@ -900,58 +831,174 @@ def test_get_all_accounts_deadlock(rpc):
|
||||
all_accounts()
|
||||
|
||||
|
||||
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 "uid/validity change folder DeltaChat" 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_broadcast(acfactory):
|
||||
@pytest.mark.parametrize("all_devices_online", [True, False])
|
||||
def test_leave_broadcast(acfactory, all_devices_online):
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
alice_chat = alice.create_broadcast("My great channel")
|
||||
snapshot = alice_chat.get_basic_snapshot()
|
||||
assert snapshot.name == "My great channel"
|
||||
assert snapshot.is_unpromoted
|
||||
assert snapshot.is_encrypted
|
||||
assert snapshot.chat_type == ChatType.OUT_BROADCAST
|
||||
bob2 = bob.clone()
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat.add_contact(alice_contact_bob)
|
||||
if all_devices_online:
|
||||
bob2.start_io()
|
||||
|
||||
alice_msg = alice_chat.send_message(text="hello").get_snapshot()
|
||||
assert alice_msg.text == "hello"
|
||||
assert alice_msg.show_padlock
|
||||
logging.info("===================== Alice creates a broadcast =====================")
|
||||
alice_chat = alice.create_broadcast("Broadcast channel!")
|
||||
|
||||
bob_msg = bob.wait_for_incoming_msg().get_snapshot()
|
||||
assert bob_msg.text == "hello"
|
||||
assert bob_msg.show_padlock
|
||||
assert bob_msg.error is None
|
||||
logging.info("===================== Bob joins the broadcast =====================")
|
||||
qr_code = alice_chat.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
bob_chat = bob.get_chat_by_id(bob_msg.chat_id)
|
||||
bob_chat_snapshot = bob_chat.get_basic_snapshot()
|
||||
assert bob_chat_snapshot.name == "My great channel"
|
||||
assert not bob_chat_snapshot.is_unpromoted
|
||||
assert bob_chat_snapshot.is_encrypted
|
||||
assert bob_chat_snapshot.chat_type == ChatType.IN_BROADCAST
|
||||
assert bob_chat_snapshot.is_contact_request
|
||||
alice_bob_contact = alice.create_contact(bob)
|
||||
alice_contacts = alice_chat.get_contacts()
|
||||
assert len(alice_contacts) == 1 # 1 recipient
|
||||
assert alice_contacts[0].id == alice_bob_contact.id
|
||||
|
||||
assert not bob_chat.can_send()
|
||||
member_added_msg = bob.wait_for_incoming_msg()
|
||||
assert member_added_msg.get_snapshot().text == "You joined the channel."
|
||||
|
||||
def get_broadcast(ac):
|
||||
chat = ac.get_chatlist(query="Broadcast channel!")[0]
|
||||
assert chat.get_basic_snapshot().name == "Broadcast channel!"
|
||||
return chat
|
||||
|
||||
def check_account(ac, contact, inviter_side, please_wait_info_msg=False):
|
||||
chat = get_broadcast(ac)
|
||||
contact_snapshot = contact.get_snapshot()
|
||||
chat_msgs = chat.get_messages()
|
||||
|
||||
if please_wait_info_msg:
|
||||
first_msg = chat_msgs.pop(0).get_snapshot()
|
||||
assert first_msg.text == "Establishing guaranteed end-to-end encryption, please wait…"
|
||||
assert first_msg.is_info
|
||||
|
||||
encrypted_msg = chat_msgs.pop(0).get_snapshot()
|
||||
assert encrypted_msg.text == "Messages are end-to-end encrypted."
|
||||
assert encrypted_msg.is_info
|
||||
|
||||
member_added_msg = chat_msgs.pop(0).get_snapshot()
|
||||
if inviter_side:
|
||||
assert member_added_msg.text == f"Member {contact_snapshot.display_name} added."
|
||||
else:
|
||||
assert member_added_msg.text == "You joined the channel."
|
||||
assert member_added_msg.is_info
|
||||
|
||||
if not inviter_side:
|
||||
leave_msg = chat_msgs.pop(0).get_snapshot()
|
||||
assert leave_msg.text == "You left the channel."
|
||||
|
||||
assert len(chat_msgs) == 0
|
||||
|
||||
chat_snapshot = chat.get_full_snapshot()
|
||||
|
||||
# On Alice's side, SELF is not in the list of contact ids
|
||||
# because OutBroadcast chats never contain SELF in the list.
|
||||
# On Bob's side, SELF is not in the list because he left.
|
||||
if inviter_side:
|
||||
assert len(chat_snapshot.contact_ids) == 0
|
||||
else:
|
||||
assert chat_snapshot.contact_ids == [contact.id]
|
||||
|
||||
logging.info("===================== Bob leaves the broadcast =====================")
|
||||
bob_chat = get_broadcast(bob)
|
||||
assert bob_chat.get_full_snapshot().self_in_group
|
||||
assert len(bob_chat.get_contacts()) == 2 # Alice and Bob
|
||||
|
||||
bob_chat.leave()
|
||||
assert not bob_chat.get_full_snapshot().self_in_group
|
||||
# After Bob left, only Alice will be left in Bob's memberlist
|
||||
assert len(bob_chat.get_contacts()) == 1
|
||||
|
||||
check_account(bob, bob.create_contact(alice), inviter_side=False, please_wait_info_msg=True)
|
||||
|
||||
logging.info("===================== Test Alice's device =====================")
|
||||
while len(alice_chat.get_contacts()) != 0: # After Bob left, there will be 0 recipients
|
||||
alice.wait_for_event(EventType.CHAT_MODIFIED)
|
||||
|
||||
check_account(alice, alice.create_contact(bob), inviter_side=True)
|
||||
|
||||
logging.info("===================== Test Bob's second device =====================")
|
||||
# Start second Bob device, if it wasn't started already.
|
||||
bob2.start_io()
|
||||
|
||||
member_added_msg = bob2.wait_for_incoming_msg()
|
||||
assert member_added_msg.get_snapshot().text == "You joined the channel."
|
||||
|
||||
bob2_chat = get_broadcast(bob2)
|
||||
|
||||
# After Bob left, only Alice will be left in Bob's memberlist
|
||||
while len(bob2_chat.get_contacts()) != 1:
|
||||
bob2.wait_for_event(EventType.CHAT_MODIFIED)
|
||||
|
||||
check_account(bob2, bob2.create_contact(alice), inviter_side=False)
|
||||
|
||||
|
||||
def test_immediate_autodelete(acfactory, direct_imap, log):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
# "1" means delete immediately, while "0" means do not delete
|
||||
ac2.set_config("delete_server_after", "1")
|
||||
|
||||
log.section("ac1: create chat with ac2")
|
||||
chat1 = ac1.create_chat(ac2)
|
||||
ac2.create_chat(ac1)
|
||||
|
||||
log.section("ac1: send message to ac2")
|
||||
sent_msg = chat1.send_text("hello")
|
||||
|
||||
msg = ac2.wait_for_incoming_msg()
|
||||
assert msg.get_snapshot().text == "hello"
|
||||
|
||||
log.section("ac2: wait for close/expunge on autodelete")
|
||||
ac2.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
|
||||
while True:
|
||||
event = ac2.wait_for_event()
|
||||
if event.kind == EventType.INFO and "Close/expunge succeeded." in event.msg:
|
||||
break
|
||||
|
||||
log.section("ac2: check that message was autodeleted on server")
|
||||
ac2_direct_imap = direct_imap(ac2)
|
||||
assert len(ac2_direct_imap.get_all_messages()) == 0
|
||||
|
||||
log.section("ac2: Mark deleted message as seen and check that read receipt arrives")
|
||||
msg.mark_seen()
|
||||
ev = ac1.wait_for_event(EventType.MSG_READ)
|
||||
assert ev.chat_id == chat1.id
|
||||
assert ev.msg_id == sent_msg.id
|
||||
|
||||
|
||||
def test_background_fetch(acfactory, dc):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.stop_io()
|
||||
|
||||
ac1_chat = ac1.create_chat(ac2)
|
||||
|
||||
ac2_chat = ac2.create_chat(ac1)
|
||||
ac2_chat.send_text("Hello!")
|
||||
|
||||
while True:
|
||||
dc.background_fetch(300)
|
||||
messages = ac1_chat.get_messages()
|
||||
snapshot = messages[-1].get_snapshot()
|
||||
if snapshot.text == "Hello!":
|
||||
break
|
||||
|
||||
# Stopping background fetch immediately after starting
|
||||
# does not result in any errors.
|
||||
background_fetch_future = dc.background_fetch.future(300)
|
||||
dc.stop_background_fetch()
|
||||
background_fetch_future()
|
||||
|
||||
# Starting background fetch with zero timeout is ok,
|
||||
# it should terminate immediately.
|
||||
dc.background_fetch(0)
|
||||
|
||||
# Background fetch can still be used to send and receive messages.
|
||||
ac2_chat.send_text("Hello again!")
|
||||
|
||||
while True:
|
||||
dc.background_fetch(300)
|
||||
messages = ac1_chat.get_messages()
|
||||
snapshot = messages[-1].get_snapshot()
|
||||
if snapshot.text == "Hello again!":
|
||||
break
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
def test_vcard(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice, bob, fiona = acfactory.get_online_accounts(3)
|
||||
|
||||
bob.create_chat(alice)
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_contact_charlie = alice.create_contact("charlie@example.org", "Charlie")
|
||||
alice_contact_charlie_snapshot = alice_contact_charlie.get_snapshot()
|
||||
alice_contact_fiona = alice.create_contact(fiona, "Fiona")
|
||||
alice_contact_fiona_snapshot = alice_contact_fiona.get_snapshot()
|
||||
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_contact(alice_contact_charlie)
|
||||
@@ -12,3 +16,12 @@ def test_vcard(acfactory) -> None:
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.vcard_contact
|
||||
assert snapshot.vcard_contact.addr == "charlie@example.org"
|
||||
assert snapshot.vcard_contact.color == alice_contact_charlie_snapshot.color
|
||||
|
||||
alice_chat_bob.send_contact(alice_contact_fiona)
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
message = bob.get_message_by_id(event.msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.vcard_contact
|
||||
assert snapshot.vcard_contact.key
|
||||
assert snapshot.vcard_contact.color == alice_contact_fiona_snapshot.color
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.21.0"
|
||||
version = "2.27.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.21.0"
|
||||
"version": "2.27.0"
|
||||
}
|
||||
|
||||
@@ -20,6 +20,11 @@ impl SystemTimeTools {
|
||||
pub fn shift(duration: Duration) {
|
||||
*SYSTEM_TIME_SHIFT.write().unwrap() += duration;
|
||||
}
|
||||
|
||||
/// Simulates the system clock being rewound by `duration`.
|
||||
pub fn shift_back(duration: Duration) {
|
||||
*SYSTEM_TIME_SHIFT.write().unwrap() -= duration;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -36,8 +36,6 @@ skip = [
|
||||
{ name = "rand_chacha", version = "0.3.1" },
|
||||
{ name = "rand_core", version = "0.6.4" },
|
||||
{ name = "rand", version = "0.8.5" },
|
||||
{ name = "redox_syscall", version = "0.3.5" },
|
||||
{ name = "redox_syscall", version = "0.4.1" },
|
||||
{ name = "rustix", version = "0.38.44" },
|
||||
{ name = "serdect", version = "0.2.0" },
|
||||
{ name = "spin", version = "0.9.8" },
|
||||
|
||||
24
flake.lock
generated
24
flake.lock
generated
@@ -47,11 +47,11 @@
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1747291057,
|
||||
"narHash": "sha256-9Wir6aLJAeJKqdoQUiwfKdBn7SyNXTJGRSscRyVOo2Y=",
|
||||
"lastModified": 1763275509,
|
||||
"narHash": "sha256-DBlu2+xPvGBaNn4RbNaw7r62lzBrf/tOKLgMYlEYhvg=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "76ffc1b7b3ec8078fe01794628b6abff35cbda8f",
|
||||
"rev": "947fdabcc3a51cec1e38641a11d4cb655fe252e7",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -147,11 +147,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1747179050,
|
||||
"narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=",
|
||||
"lastModified": 1762977756,
|
||||
"narHash": "sha256-4PqRErxfe+2toFJFgcRKZ0UI9NSIOJa+7RXVtBhy4KE=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e",
|
||||
"rev": "c5ae371f1a6a7fd27823bc500d9390b38c05fa55",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -175,11 +175,11 @@
|
||||
},
|
||||
"nixpkgs_4": {
|
||||
"locked": {
|
||||
"lastModified": 1747179050,
|
||||
"narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=",
|
||||
"lastModified": 1763283776,
|
||||
"narHash": "sha256-Y7TDFPK4GlqrKrivOcsHG8xSGqQx3A6c+i7novT85Uk=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e",
|
||||
"rev": "50a96edd8d0db6cc8db57dab6bb6d6ee1f3dc49a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -202,11 +202,11 @@
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1746889290,
|
||||
"narHash": "sha256-h3LQYZgyv2l3U7r+mcsrEOGRldaK0zJFwAAva4hV/6g=",
|
||||
"lastModified": 1762860488,
|
||||
"narHash": "sha256-rMfWMCOo/pPefM2We0iMBLi2kLBAnYoB9thi4qS7uk4=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "2bafe9d96c6734aacfd49e115f6cf61e7adc68bc",
|
||||
"rev": "2efc80078029894eec0699f62ec8d5c1a56af763",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
12
flake.nix
12
flake.nix
@@ -34,7 +34,6 @@
|
||||
./Cargo.lock
|
||||
./Cargo.toml
|
||||
./CMakeLists.txt
|
||||
./CONTRIBUTING.md
|
||||
./deltachat_derive
|
||||
./deltachat-contact-tools
|
||||
./deltachat-ffi
|
||||
@@ -121,12 +120,14 @@
|
||||
version = manifest.version;
|
||||
strictDeps = true;
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
buildInputs = [
|
||||
pkgsWin64.windows.pthreads
|
||||
];
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
];
|
||||
depsBuildBuild = [
|
||||
pkgsWin64.stdenv.cc
|
||||
pkgsWin64.windows.pthreads
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
@@ -184,12 +185,14 @@
|
||||
version = manifest.version;
|
||||
strictDeps = true;
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
buildInputs = [
|
||||
pkgsWin32.windows.pthreads
|
||||
];
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
];
|
||||
depsBuildBuild = [
|
||||
winCC
|
||||
pkgsWin32.windows.pthreads
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
@@ -237,6 +240,9 @@
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
|
||||
CARGO_TARGET_X86_64_APPLE_DARWIN_RUSTFLAGS = "-Clink-args=-L${pkgsCross.libiconv}/lib";
|
||||
CARGO_TARGET_AARCH64_APPLE_DARWIN_RUSTFLAGS = "-Clink-args=-L${pkgsCross.libiconv}/lib";
|
||||
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "2.21.0"
|
||||
version = "2.27.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.8"
|
||||
|
||||
@@ -404,18 +404,16 @@ class Account:
|
||||
self,
|
||||
name: str,
|
||||
contacts: Optional[List[Contact]] = None,
|
||||
verified: bool = False,
|
||||
) -> Chat:
|
||||
"""create a new group chat object.
|
||||
|
||||
Chats are unpromoted until the first message is sent.
|
||||
|
||||
:param contacts: list of contacts to add
|
||||
:param verified: if true only verified contacts can be added.
|
||||
:returns: a :class:`deltachat.chat.Chat` object.
|
||||
"""
|
||||
bytes_name = name.encode("utf8")
|
||||
chat_id = lib.dc_create_group_chat(self._dc_context, int(verified), bytes_name)
|
||||
chat_id = lib.dc_create_group_chat(self._dc_context, 0, bytes_name)
|
||||
chat = Chat(self, chat_id)
|
||||
if contacts is not None:
|
||||
for contact in contacts:
|
||||
|
||||
@@ -142,13 +142,6 @@ class Chat:
|
||||
"""
|
||||
return bool(lib.dc_chat_can_send(self._dc_chat))
|
||||
|
||||
def is_protected(self) -> bool:
|
||||
"""return True if this chat is a protected chat.
|
||||
|
||||
:returns: True if chat is protected, False otherwise.
|
||||
"""
|
||||
return bool(lib.dc_chat_is_protected(self._dc_chat))
|
||||
|
||||
def get_name(self) -> Optional[str]:
|
||||
"""return name of this chat.
|
||||
|
||||
|
||||
@@ -523,7 +523,6 @@ class ACFactory:
|
||||
assert "addr" in configdict and "mail_pw" in configdict, configdict
|
||||
configdict.setdefault("bcc_self", False)
|
||||
configdict.setdefault("mvbox_move", False)
|
||||
configdict.setdefault("sentbox_watch", False)
|
||||
configdict.setdefault("sync_msgs", False)
|
||||
configdict.setdefault("delete_server_after", 0)
|
||||
ac.update_config(configdict)
|
||||
@@ -604,20 +603,6 @@ class ACFactory:
|
||||
ac2.create_chat(ac1)
|
||||
return ac1.create_chat(ac2)
|
||||
|
||||
def get_protected_chat(self, ac1: Account, ac2: Account):
|
||||
chat = ac1.create_group_chat("Protected Group", verified=True)
|
||||
qr = chat.get_join_qr()
|
||||
ac2.qr_join_chat(qr)
|
||||
ac2._evtracker.wait_securejoin_joiner_progress(1000)
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
msg = ac2.get_message_by_id(ev.data2)
|
||||
assert msg is not None
|
||||
assert msg.text == "Messages are end-to-end encrypted."
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg is not None
|
||||
assert "Member Me " in msg.text and " added by " in msg.text
|
||||
return chat
|
||||
|
||||
def introduce_each_other(self, accounts, sending=True):
|
||||
to_wait = []
|
||||
for i, acc in enumerate(accounts):
|
||||
|
||||
@@ -116,10 +116,8 @@ class TestGroupStressTests:
|
||||
|
||||
def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
ac1_addr = ac1.get_self_contact().addr
|
||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat1 = ac1.create_group_chat("hello", verified=True)
|
||||
assert chat1.is_protected()
|
||||
chat1 = ac1.create_group_chat("hello")
|
||||
qr = chat1.get_join_qr()
|
||||
lp.sec("ac2: start QR-code based join-group protocol")
|
||||
chat2 = ac2.qr_join_chat(qr)
|
||||
@@ -142,7 +140,6 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
lp.sec("ac2: read message and check that it's a verified chat")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
assert msg.chat.is_protected()
|
||||
assert msg.is_encrypted()
|
||||
|
||||
lp.sec("ac2: Check that ac2 verified ac1")
|
||||
@@ -173,8 +170,12 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
lp.sec("ac2: Check that ac1 verified ac3 for ac2")
|
||||
ac2_ac1_contact = ac2.get_contacts()[0]
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact).id == dc.const.DC_CONTACT_ID_SELF
|
||||
ac2_ac3_contact = ac2.get_contacts()[1]
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac3_contact).addr == ac1_addr
|
||||
for ac2_contact in chat2.get_contacts():
|
||||
if ac2_contact == ac2_ac1_contact or ac2_contact.id == dc.const.DC_CONTACT_ID_SELF:
|
||||
continue
|
||||
# Until we reset verifications and then send the _verified header,
|
||||
# verification is not gossiped here:
|
||||
assert ac2.get_self_contact().get_verifier(ac2_contact) is None
|
||||
|
||||
lp.sec("ac2: send message and let ac3 read it")
|
||||
chat2.send_text("hi")
|
||||
@@ -266,8 +267,7 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
|
||||
ac1_offl.stop_io()
|
||||
|
||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat = ac1.create_group_chat("hello", verified=True)
|
||||
assert chat.is_protected()
|
||||
chat = ac1.create_group_chat("hello")
|
||||
qr = chat.get_join_qr()
|
||||
lp.sec("ac2: start QR-code based join-group protocol")
|
||||
chat2 = ac2.qr_join_chat(qr)
|
||||
@@ -321,8 +321,7 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
|
||||
ac1.set_avatar(avatar_path)
|
||||
|
||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat = ac1.create_group_chat("hello", verified=True)
|
||||
assert chat.is_protected()
|
||||
chat = ac1.create_group_chat("hello")
|
||||
qr = chat.get_join_qr()
|
||||
lp.sec("ac2: start QR-code based join-group protocol")
|
||||
ac2.qr_join_chat(qr)
|
||||
@@ -336,7 +335,6 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
|
||||
assert msg_in.is_system_message()
|
||||
assert contact.addr == ac1.get_config("addr")
|
||||
chat2 = msg_in.chat
|
||||
assert chat2.is_protected()
|
||||
assert chat2.get_messages()[0].text == "Messages are end-to-end encrypted."
|
||||
assert open(contact.get_profile_image(), "rb").read() == open(avatar_path, "rb").read()
|
||||
|
||||
@@ -376,8 +374,7 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
|
||||
ac2_offl.stop_io()
|
||||
|
||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat1 = ac1.create_group_chat("hello", verified=True)
|
||||
assert chat1.is_protected()
|
||||
chat1 = ac1.create_group_chat("hello")
|
||||
qr = chat1.get_join_qr()
|
||||
lp.sec("ac2: start QR-code based join-group protocol")
|
||||
chat2 = ac2.qr_join_chat(qr)
|
||||
@@ -402,29 +399,20 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
|
||||
assert ac2_offl_ac1_contact.addr == ac1.get_config("addr")
|
||||
assert not ac2_offl_ac1_contact.is_verified()
|
||||
chat2_offl = msg_in.chat
|
||||
assert not chat2_offl.is_protected()
|
||||
|
||||
lp.sec("ac2: sending message re-gossiping Autocrypt keys")
|
||||
chat2.send_text("hi2")
|
||||
|
||||
lp.sec("ac2_offl: receiving message")
|
||||
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg_in = ac2_offl.get_message_by_id(ev.data2)
|
||||
assert msg_in.is_system_message()
|
||||
assert msg_in.text == "Messages are end-to-end encrypted."
|
||||
|
||||
# We need to consume one event that has data2=0
|
||||
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
assert ev.data2 == 0
|
||||
|
||||
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg_in = ac2_offl.get_message_by_id(ev.data2)
|
||||
assert not msg_in.is_system_message()
|
||||
assert msg_in.text == "hi2"
|
||||
assert msg_in.chat == chat2_offl
|
||||
assert msg_in.get_sender_contact().addr == ac2.get_config("addr")
|
||||
assert msg_in.chat.is_protected()
|
||||
assert ac2_offl_ac1_contact.is_verified()
|
||||
# Until we reset verifications and then send the _verified header,
|
||||
# verification is not gossiped here:
|
||||
assert not ac2_offl_ac1_contact.is_verified()
|
||||
|
||||
|
||||
def test_deleted_msgs_dont_reappear(acfactory):
|
||||
|
||||
@@ -5,7 +5,7 @@ import base64
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
from imap_tools import AND, U
|
||||
from imap_tools import AND
|
||||
|
||||
import deltachat as dc
|
||||
from deltachat import account_hookimpl, Message
|
||||
@@ -269,94 +269,6 @@ def test_enable_mvbox_move(acfactory, lp):
|
||||
assert ac2._evtracker.wait_next_incoming_message().text == "message1"
|
||||
|
||||
|
||||
def test_mvbox_sentbox_threads(acfactory, lp):
|
||||
lp.sec("ac1: start with mvbox thread")
|
||||
ac1 = acfactory.new_online_configuring_account(mvbox_move=True, sentbox_watch=False)
|
||||
|
||||
lp.sec("ac2: start without mvbox/sentbox threads")
|
||||
ac2 = acfactory.new_online_configuring_account(mvbox_move=False, sentbox_watch=False)
|
||||
|
||||
lp.sec("ac2 and ac1: waiting for configuration")
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
lp.sec("ac1: create and configure sentbox")
|
||||
ac1.direct_imap.create_folder("Sent")
|
||||
ac1.set_config("sentbox_watch", "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"
|
||||
|
||||
assert ac1.get_config("configured_mvbox_folder") == "DeltaChat"
|
||||
while ac1.get_config("configured_sentbox_folder") != "Sent":
|
||||
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
|
||||
|
||||
|
||||
def test_move_works(acfactory):
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac2 = acfactory.new_online_configuring_account(mvbox_move=True)
|
||||
acfactory.bring_accounts_online()
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
chat.send_text("message1")
|
||||
|
||||
# Message is moved to the movebox
|
||||
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
|
||||
# Message is downloaded
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
|
||||
assert ev.data2 > dc.const.DC_CHAT_ID_LAST_SPECIAL
|
||||
|
||||
|
||||
def test_move_avoids_loop(acfactory):
|
||||
"""Test that the message is only moved once.
|
||||
|
||||
This is to avoid busy loop if moved message reappears in the Inbox
|
||||
or some scanned folder later.
|
||||
For example, this happens on servers that alias `INBOX.DeltaChat` to `DeltaChat` folder,
|
||||
so the message moved to `DeltaChat` appears as a new message in the `INBOX.DeltaChat` folder.
|
||||
We do not want to move this message from `INBOX.DeltaChat` to `DeltaChat` again.
|
||||
"""
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac2 = acfactory.new_online_configuring_account(mvbox_move=True)
|
||||
acfactory.bring_accounts_online()
|
||||
ac1_chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
ac1_chat.send_text("Message 1")
|
||||
|
||||
# Message is moved to the DeltaChat folder and downloaded.
|
||||
ac2_msg1 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert ac2_msg1.text == "Message 1"
|
||||
|
||||
# Move the message to the INBOX again.
|
||||
ac2.direct_imap.select_folder("DeltaChat")
|
||||
ac2.direct_imap.conn.move(["*"], "INBOX")
|
||||
|
||||
ac1_chat.send_text("Message 2")
|
||||
ac2_msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert ac2_msg2.text == "Message 2"
|
||||
|
||||
# Check that Message 1 is still in the INBOX folder
|
||||
# and Message 2 is in the DeltaChat folder.
|
||||
ac2.direct_imap.select_folder("INBOX")
|
||||
assert len(ac2.direct_imap.get_all_messages()) == 1
|
||||
ac2.direct_imap.select_folder("DeltaChat")
|
||||
assert len(ac2.direct_imap.get_all_messages()) == 1
|
||||
|
||||
|
||||
def test_move_works_on_self_sent(acfactory):
|
||||
ac1 = acfactory.new_online_configuring_account(mvbox_move=True)
|
||||
ac2 = acfactory.new_online_configuring_account()
|
||||
acfactory.bring_accounts_online()
|
||||
ac1.set_config("bcc_self", "1")
|
||||
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
chat.send_text("message1")
|
||||
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
chat.send_text("message2")
|
||||
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
chat.send_text("message3")
|
||||
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
|
||||
|
||||
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()
|
||||
@@ -442,7 +354,7 @@ def test_forward_own_message(acfactory, lp):
|
||||
|
||||
def test_resend_message(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat1 = ac1.create_chat(ac2)
|
||||
chat1 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
lp.sec("ac1: send message to ac2")
|
||||
chat1.send_text("message")
|
||||
@@ -450,14 +362,19 @@ def test_resend_message(acfactory, lp):
|
||||
lp.sec("ac2: receive message")
|
||||
msg_in = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg_in.text == "message"
|
||||
chat2 = msg_in.chat
|
||||
chat2_msg_cnt = len(chat2.get_messages())
|
||||
|
||||
lp.sec("ac1: resend message")
|
||||
ac1.resend_messages([msg_in])
|
||||
|
||||
lp.sec("ac2: check that message is deleted")
|
||||
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
|
||||
lp.sec("ac1: send another message")
|
||||
chat1.send_text("another message")
|
||||
|
||||
lp.sec("ac2: receive another message")
|
||||
msg_in = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg_in.text == "another message"
|
||||
chat2 = msg_in.chat
|
||||
chat2_msg_cnt = len(chat2.get_messages())
|
||||
|
||||
assert len(chat2.get_messages()) == chat2_msg_cnt
|
||||
|
||||
|
||||
@@ -584,39 +501,6 @@ def test_send_and_receive_message_markseen(acfactory, lp):
|
||||
pass # mark_seen_messages() has generated events before it returns
|
||||
|
||||
|
||||
def test_moved_markseen(acfactory):
|
||||
"""Test that message already moved to DeltaChat folder is marked as seen."""
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac2 = acfactory.new_online_configuring_account(mvbox_move=True)
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
ac2.stop_io()
|
||||
with ac2.direct_imap.idle() as idle2:
|
||||
ac1.create_chat(ac2).send_text("Hello!")
|
||||
idle2.wait_for_new_message()
|
||||
|
||||
# Emulate moving of the message to DeltaChat folder by Sieve rule.
|
||||
ac2.direct_imap.conn.move(["*"], "DeltaChat")
|
||||
ac2.direct_imap.select_folder("DeltaChat")
|
||||
|
||||
with ac2.direct_imap.idle() as idle2:
|
||||
ac2.start_io()
|
||||
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg = ac2.get_message_by_id(ev.data2)
|
||||
assert msg.text == "Messages are end-to-end encrypted."
|
||||
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg = ac2.get_message_by_id(ev.data2)
|
||||
|
||||
# Accept the contact request.
|
||||
msg.chat.accept()
|
||||
ac2.mark_seen_messages([msg])
|
||||
uid = idle2.wait_for_seen()
|
||||
|
||||
assert len(list(ac2.direct_imap.conn.fetch(AND(seen=True, uid=U(uid, "*"))))) == 1
|
||||
|
||||
|
||||
def test_message_override_sender_name(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("displayname", "ac1-default-displayname")
|
||||
@@ -651,36 +535,6 @@ def test_message_override_sender_name(acfactory, lp):
|
||||
assert not msg2.override_sender_name
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mvbox_move", [True, False])
|
||||
def test_markseen_message_and_mdn(acfactory, mvbox_move):
|
||||
# Please only change this test if you are very sure that it will still catch the issues it catches now.
|
||||
# We had so many problems with markseen, if in doubt, rather create another test, it can't harm.
|
||||
ac1 = acfactory.new_online_configuring_account(mvbox_move=mvbox_move)
|
||||
ac2 = acfactory.new_online_configuring_account(mvbox_move=mvbox_move)
|
||||
acfactory.bring_accounts_online()
|
||||
# Do not send BCC to self, we only want to test MDN on ac1.
|
||||
ac1.set_config("bcc_self", "0")
|
||||
|
||||
acfactory.get_accepted_chat(ac1, ac2).send_text("hi")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
ac2.mark_seen_messages([msg])
|
||||
|
||||
folder = "mvbox" if mvbox_move else "inbox"
|
||||
for ac in [ac1, ac2]:
|
||||
if mvbox_move:
|
||||
ac._evtracker.get_info_contains("Marked messages [0-9]+ in folder DeltaChat as seen.")
|
||||
else:
|
||||
ac._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
|
||||
ac1.direct_imap.select_config_folder(folder)
|
||||
ac2.direct_imap.select_config_folder(folder)
|
||||
|
||||
# Check that the mdn is marked as seen
|
||||
assert len(list(ac1.direct_imap.conn.fetch(AND(seen=True)))) == 1
|
||||
# Check original message is marked as seen
|
||||
assert len(list(ac2.direct_imap.conn.fetch(AND(seen=True)))) == 1
|
||||
|
||||
|
||||
def test_reply_privately(acfactory):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -830,156 +684,6 @@ def test_no_draft_if_cant_send(acfactory):
|
||||
assert device_chat.get_draft() is None
|
||||
|
||||
|
||||
def test_dont_show_emails(acfactory, lp):
|
||||
"""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 AND it's not in the sentbox, then ignore the email.
|
||||
|
||||
If the draft email is sent out later (i.e. moved to "Sent"), it must be shown.
|
||||
|
||||
Also, test that unknown emails in the Spam folder are not shown."""
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac1.set_config("show_emails", "2")
|
||||
ac1.create_contact("alice@example.org").create_chat()
|
||||
|
||||
acfactory.wait_configured(ac1)
|
||||
ac1.direct_imap.create_folder("Drafts")
|
||||
ac1.direct_imap.create_folder("Sent")
|
||||
ac1.direct_imap.create_folder("Spam")
|
||||
ac1.direct_imap.create_folder("Junk")
|
||||
|
||||
acfactory.bring_accounts_online()
|
||||
ac1.stop_io()
|
||||
|
||||
ac1.direct_imap.append(
|
||||
"Drafts",
|
||||
"""
|
||||
From: ac1 <{}>
|
||||
Subject: subj
|
||||
To: alice@example.org
|
||||
Message-ID: <aepiors@example.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
message in Drafts that is moved to Sent later
|
||||
""".format(
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
ac1.direct_imap.append(
|
||||
"Sent",
|
||||
"""
|
||||
From: ac1 <{}>
|
||||
Subject: subj
|
||||
To: alice@example.org
|
||||
Message-ID: <hsabaeni@example.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
message in Sent
|
||||
""".format(
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
ac1.direct_imap.append(
|
||||
"Spam",
|
||||
"""
|
||||
From: unknown.address@junk.org
|
||||
Subject: subj
|
||||
To: {}
|
||||
Message-ID: <spam.message@junk.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Unknown message in Spam
|
||||
""".format(
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
ac1.direct_imap.append(
|
||||
"Spam",
|
||||
"""
|
||||
From: unknown.address@junk.org, unkwnown.add@junk.org
|
||||
Subject: subj
|
||||
To: {}
|
||||
Message-ID: <spam.message2@junk.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Unknown & malformed message in Spam
|
||||
""".format(
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
ac1.direct_imap.append(
|
||||
"Spam",
|
||||
"""
|
||||
From: delta<address: inbox@nhroy.com>
|
||||
Subject: subj
|
||||
To: {}
|
||||
Message-ID: <spam.message99@junk.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Unknown & malformed message in Spam
|
||||
""".format(
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
ac1.direct_imap.append(
|
||||
"Spam",
|
||||
"""
|
||||
From: alice@example.org
|
||||
Subject: subj
|
||||
To: {}
|
||||
Message-ID: <spam.message3@junk.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Actually interesting message in Spam
|
||||
""".format(
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
ac1.direct_imap.append(
|
||||
"Junk",
|
||||
"""
|
||||
From: unknown.address@junk.org
|
||||
Subject: subj
|
||||
To: {}
|
||||
Message-ID: <spam.message@junk.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Unknown message in Junk
|
||||
""".format(
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
|
||||
ac1.set_config("scan_all_folders_debounce_secs", "0")
|
||||
lp.sec("All prepared, now let DC find the message")
|
||||
ac1.start_io()
|
||||
|
||||
msg = ac1._evtracker.wait_next_messages_changed()
|
||||
|
||||
# Wait until each folder was scanned, this is necessary for this test to test what it should test:
|
||||
ac1._evtracker.wait_idle_inbox_ready()
|
||||
|
||||
assert msg.text == "subj – message in Sent"
|
||||
chat_msgs = msg.chat.get_messages()
|
||||
assert len(chat_msgs) == 2
|
||||
assert any(msg.text == "subj – Actually interesting message in Spam" for msg in chat_msgs)
|
||||
|
||||
assert not any("unknown.address" in c.get_name() for c in ac1.get_chats())
|
||||
ac1.direct_imap.select_folder("Spam")
|
||||
assert ac1.direct_imap.get_uid_by_message_id("spam.message@junk.org")
|
||||
|
||||
ac1.stop_io()
|
||||
lp.sec("'Send out' the draft, i.e. move it to the Sent folder, and wait for DC to display it this time")
|
||||
ac1.direct_imap.select_folder("Drafts")
|
||||
uid = ac1.direct_imap.get_uid_by_message_id("aepiors@example.org")
|
||||
ac1.direct_imap.conn.move(uid, "Sent")
|
||||
|
||||
ac1.start_io()
|
||||
msg2 = ac1._evtracker.wait_next_messages_changed()
|
||||
|
||||
assert msg2.text == "subj – message in Drafts that is moved to Sent later"
|
||||
assert len(msg.chat.get_messages()) == 3
|
||||
|
||||
|
||||
def test_bot(acfactory, lp):
|
||||
"""Test that bot messages can be identified as such"""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
@@ -1204,7 +908,7 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
|
||||
|
||||
def test_qr_email_capitalization(acfactory, lp):
|
||||
"""Regression test for a bug
|
||||
that resulted in failure to propagate verification via gossip in a verified group
|
||||
that resulted in failure to propagate verification
|
||||
when the database already contained the contact with a different email address capitalization.
|
||||
"""
|
||||
|
||||
@@ -1215,24 +919,27 @@ def test_qr_email_capitalization(acfactory, lp):
|
||||
lp.sec(f"ac1 creates a contact for ac2 ({ac2_addr_uppercase})")
|
||||
ac1.create_contact(ac2_addr_uppercase)
|
||||
|
||||
lp.sec("ac3 creates a verified group with a QR code")
|
||||
chat = ac3.create_group_chat("hello", verified=True)
|
||||
lp.sec("ac3 creates a group with a QR code")
|
||||
chat = ac3.create_group_chat("hello")
|
||||
qr = chat.get_join_qr()
|
||||
|
||||
lp.sec("ac1 joins a verified group via a QR code")
|
||||
lp.sec("ac1 joins a group via a QR code")
|
||||
ac1_chat = ac1.qr_join_chat(qr)
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "Member Me added by {}.".format(ac3.get_config("addr"))
|
||||
assert len(ac1_chat.get_contacts()) == 2
|
||||
|
||||
lp.sec("ac2 joins a verified group via a QR code")
|
||||
lp.sec("ac2 joins a group via a QR code")
|
||||
ac2.qr_join_chat(qr)
|
||||
ac1._evtracker.wait_next_incoming_message()
|
||||
|
||||
# ac1 should see both ac3 and ac2 as verified.
|
||||
assert len(ac1_chat.get_contacts()) == 3
|
||||
# Until we reset verifications and then send the _verified header,
|
||||
# the verification of ac2 is not gossiped here:
|
||||
for contact in ac1_chat.get_contacts():
|
||||
assert contact.is_verified()
|
||||
is_ac2 = contact.addr == ac2.get_config("addr")
|
||||
assert contact.is_verified() != is_ac2
|
||||
|
||||
|
||||
def test_set_get_contact_avatar(acfactory, data, lp):
|
||||
@@ -1499,9 +1206,15 @@ def test_send_receive_locations(acfactory, lp):
|
||||
assert locations[0].latitude == 2.0
|
||||
assert locations[0].longitude == 3.0
|
||||
assert locations[0].accuracy == 0.5
|
||||
assert locations[0].timestamp > now
|
||||
assert locations[0].marker is None
|
||||
|
||||
# Make sure the timestamp is not in the past.
|
||||
# Note that location timestamp has only 1 second precision,
|
||||
# while `now` has a fractional part, so we have to truncate it
|
||||
# first, otherwise `now` may appear to be in the future
|
||||
# even though it is the same second.
|
||||
assert int(locations[0].timestamp.timestamp()) >= int(now.timestamp())
|
||||
|
||||
contact = ac2.create_contact(ac1)
|
||||
locations2 = chat2.get_locations(contact=contact)
|
||||
assert len(locations2) == 1
|
||||
@@ -1512,38 +1225,6 @@ def test_send_receive_locations(acfactory, lp):
|
||||
assert not locations3
|
||||
|
||||
|
||||
def test_immediate_autodelete(acfactory, lp):
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac2 = acfactory.new_online_configuring_account()
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
# "1" means delete immediately, while "0" means do not delete
|
||||
ac2.set_config("delete_server_after", "1")
|
||||
|
||||
lp.sec("ac1: create chat with ac2")
|
||||
chat1 = ac1.create_chat(ac2)
|
||||
ac2.create_chat(ac1)
|
||||
|
||||
lp.sec("ac1: send message to ac2")
|
||||
sent_msg = chat1.send_text("hello")
|
||||
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
|
||||
lp.sec("ac2: wait for close/expunge on autodelete")
|
||||
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
|
||||
ac2._evtracker.get_info_contains("Close/expunge succeeded.")
|
||||
|
||||
lp.sec("ac2: check that message was autodeleted on server")
|
||||
assert len(ac2.direct_imap.get_all_messages()) == 0
|
||||
|
||||
lp.sec("ac2: Mark deleted message as seen and check that read receipt arrives")
|
||||
msg.mark_seen()
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_READ")
|
||||
assert ev.data1 == chat1.id
|
||||
assert ev.data2 == sent_msg.id
|
||||
|
||||
|
||||
def test_delete_multiple_messages(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat12 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
@@ -1576,55 +1257,6 @@ def test_delete_multiple_messages(acfactory, lp):
|
||||
break
|
||||
|
||||
|
||||
def test_trash_multiple_messages(acfactory, lp):
|
||||
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.
|
||||
lp.sec("Creating trash folder")
|
||||
ac2.direct_imap.create_folder("Trash")
|
||||
ac2.set_config("delete_to_trash", "1")
|
||||
|
||||
lp.sec("Check that Trash can be configured initially as well")
|
||||
ac3 = acfactory.new_online_configuring_account(cloned_from=ac2)
|
||||
acfactory.bring_accounts_online()
|
||||
assert ac3.get_config("configured_trash_folder")
|
||||
ac3.stop_io()
|
||||
|
||||
ac2.start_io()
|
||||
chat12 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
lp.sec("ac1: sending 3 messages")
|
||||
texts = ["first", "second", "third"]
|
||||
for text in texts:
|
||||
chat12.send_text(text)
|
||||
|
||||
lp.sec("ac2: waiting for all messages on the other side")
|
||||
to_delete = []
|
||||
for text in texts:
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
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"
|
||||
|
||||
lp.sec("ac2: deleting all messages except second")
|
||||
assert len(to_delete) == len(texts) - 1
|
||||
ac2.delete_messages(to_delete)
|
||||
|
||||
lp.sec("ac2: test that only one message is left")
|
||||
while 1:
|
||||
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
ac2.direct_imap.select_config_folder("inbox")
|
||||
nr_msgs = len(ac2.direct_imap.get_all_messages())
|
||||
assert nr_msgs > 0
|
||||
if nr_msgs == 1:
|
||||
break
|
||||
|
||||
|
||||
def test_configure_error_msgs_wrong_pw(acfactory):
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
|
||||
@@ -1748,71 +1380,6 @@ def test_group_quote(acfactory, lp):
|
||||
assert received_reply.quote.id == out_msg.id
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("folder", "move", "expected_destination"),
|
||||
[
|
||||
(
|
||||
"xyz",
|
||||
False,
|
||||
"xyz",
|
||||
), # Test that emails aren't found in a random folder
|
||||
(
|
||||
"Spam",
|
||||
True,
|
||||
"DeltaChat",
|
||||
), # ...emails are moved from the spam folder to "DeltaChat"
|
||||
(
|
||||
"Spam",
|
||||
False,
|
||||
"INBOX",
|
||||
), # ...emails are moved from the spam folder to the Inbox
|
||||
],
|
||||
)
|
||||
# Testrun.org does not support the CREATE-SPECIAL-USE capability, which means that we can't create a folder with
|
||||
# the "\Junk" flag (see https://tools.ietf.org/html/rfc6154). So, we can't test spam folder detection by flag.
|
||||
def test_scan_folders(acfactory, lp, folder, move, expected_destination):
|
||||
"""Delta Chat periodically scans all folders for new messages to make sure we don't miss any."""
|
||||
variant = folder + "-" + str(move) + "-" + expected_destination
|
||||
lp.sec("Testing variant " + variant)
|
||||
ac1 = acfactory.new_online_configuring_account(mvbox_move=move)
|
||||
ac2 = acfactory.new_online_configuring_account()
|
||||
|
||||
acfactory.wait_configured(ac1)
|
||||
ac1.direct_imap.create_folder(folder)
|
||||
|
||||
# Wait until each folder was selected once and we are IDLEing:
|
||||
acfactory.bring_accounts_online()
|
||||
ac1.stop_io()
|
||||
assert folder in ac1.direct_imap.list_folders()
|
||||
|
||||
lp.sec("Send a message to from ac2 to ac1 and manually move it to `folder`")
|
||||
ac1.direct_imap.select_config_folder("inbox")
|
||||
with ac1.direct_imap.idle() as idle1:
|
||||
acfactory.get_accepted_chat(ac2, ac1).send_text("hello")
|
||||
idle1.wait_for_new_message()
|
||||
ac1.direct_imap.conn.move(["*"], folder) # "*" means "biggest UID in mailbox"
|
||||
|
||||
lp.sec("start_io() and see if DeltaChat finds the message (" + variant + ")")
|
||||
ac1.set_config("scan_all_folders_debounce_secs", "0")
|
||||
ac1.start_io()
|
||||
chat = ac1.create_chat(ac2)
|
||||
n_msgs = 1 # "Messages are end-to-end encrypted."
|
||||
if folder == "Spam":
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
n_msgs += 1
|
||||
else:
|
||||
ac1._evtracker.wait_idle_inbox_ready()
|
||||
assert len(chat.get_messages()) == n_msgs
|
||||
|
||||
# The message has reached its destination.
|
||||
ac1.direct_imap.select_folder(expected_destination)
|
||||
assert len(ac1.direct_imap.get_all_messages()) == 1
|
||||
if folder != expected_destination:
|
||||
ac1.direct_imap.select_folder(folder)
|
||||
assert len(ac1.direct_imap.get_all_messages()) == 0
|
||||
|
||||
|
||||
def test_archived_muted_chat(acfactory, lp):
|
||||
"""If an archived and muted chat receives a new message, DC_EVENT_MSGS_CHANGED for
|
||||
DC_CHAT_ID_ARCHIVED_LINK must be generated if the chat had only seen messages previously.
|
||||
|
||||
@@ -35,7 +35,7 @@ class TestOfflineAccountBasic:
|
||||
d = ac1.get_info()
|
||||
assert d["arch"]
|
||||
assert d["number_of_chats"] == "0"
|
||||
assert d["bcc_self"] == "1"
|
||||
assert d["bcc_self"] == "0"
|
||||
|
||||
def test_is_not_configured(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
@@ -69,7 +69,7 @@ class TestOfflineAccountBasic:
|
||||
def test_has_bccself(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
assert "bcc_self" in ac1.get_config("sys.config_keys").split()
|
||||
assert ac1.get_config("bcc_self") == "1"
|
||||
assert ac1.get_config("bcc_self") == "0"
|
||||
|
||||
def test_selfcontact_if_unconfigured(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
@@ -271,10 +271,9 @@ class TestOfflineChat:
|
||||
chat.set_name("Homework")
|
||||
assert chat.get_messages()[-1].text == "abc homework xyz Homework"
|
||||
|
||||
@pytest.mark.parametrize("verified", [True, False])
|
||||
def test_group_chat_qr(self, acfactory, ac1, verified):
|
||||
def test_group_chat_qr(self, acfactory, ac1):
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
chat = ac1.create_group_chat(name="title1", verified=verified)
|
||||
chat = ac1.create_group_chat(name="title1")
|
||||
assert chat.is_group()
|
||||
qr = chat.get_join_qr()
|
||||
assert ac2.check_qr(qr).is_ask_verifygroup
|
||||
|
||||
@@ -23,7 +23,6 @@ deps =
|
||||
pytest
|
||||
pytest-timeout
|
||||
pytest-xdist
|
||||
pdbpp
|
||||
requests
|
||||
# urllib3 2.0 does not work in manylinux2014 containers.
|
||||
# https://github.com/deltachat/deltachat-core-rust/issues/4788
|
||||
|
||||
@@ -1 +1 @@
|
||||
2025-10-16
|
||||
2025-11-16
|
||||
@@ -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.90.0
|
||||
RUST_VERSION=1.91.0
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
|
||||
|
||||
@@ -6,7 +6,7 @@ set -euo pipefail
|
||||
export TZ=UTC
|
||||
|
||||
# Provider database revision.
|
||||
REV=1cce91c1f1065b47e4f307d6fe2f4cca68c74d2e
|
||||
REV=d041136c19a48b493823b46d472f12b9ee94ae80
|
||||
|
||||
CORE_ROOT="$PWD"
|
||||
TMP="$(mktemp -d)"
|
||||
|
||||
20
spec.md
20
spec.md
@@ -1,6 +1,6 @@
|
||||
# Chatmail Specification
|
||||
|
||||
Version: 0.36.0
|
||||
Version: 0.37.0
|
||||
Status: In-progress
|
||||
Format: [Semantic Line Breaks](https://sembr.org/)
|
||||
|
||||
@@ -582,6 +582,24 @@ and e.g. simply search for the line starting with `EMAIL`
|
||||
in order to get the email address.
|
||||
|
||||
|
||||
# Verifications
|
||||
|
||||
Keys obtained using [SecureJoin](https://securejoin.readthedocs.io) protocol
|
||||
and corresponding contacts
|
||||
are considered "verified".
|
||||
|
||||
As an extension to `Autocrypt-Gossip` header,
|
||||
chatmail clients can add `_verified=1` attribute
|
||||
(underscore marks the attribute as non-critical)
|
||||
to indicate that they have the gossiped key
|
||||
and the corresponding contact marked as verified.
|
||||
|
||||
When receiving such `Autocrypt-Gossip` header
|
||||
in a message signed by a verified key,
|
||||
chatmail clients mark the gossiped key
|
||||
as indirectly verified.
|
||||
|
||||
|
||||
# Transitioning to a new e-mail address (AEAP)
|
||||
|
||||
When receiving a message:
|
||||
|
||||
@@ -3,8 +3,12 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::future::Future;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context as _, Result, bail, ensure};
|
||||
use async_channel::{self, Receiver, Sender};
|
||||
use futures::FutureExt as _;
|
||||
use futures_lite::FutureExt as _;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
@@ -18,7 +22,7 @@ use tokio::time::{Duration, sleep};
|
||||
|
||||
use crate::context::{Context, ContextBuilder};
|
||||
use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::log::{info, warn};
|
||||
use crate::log::warn;
|
||||
use crate::push::PushSubscriber;
|
||||
use crate::stock_str::StockStrings;
|
||||
|
||||
@@ -41,6 +45,13 @@ pub struct Accounts {
|
||||
|
||||
/// Push notification subscriber shared between accounts.
|
||||
push_subscriber: PushSubscriber,
|
||||
|
||||
/// Channel sender to cancel ongoing background_fetch().
|
||||
///
|
||||
/// If background_fetch() is not running, this is `None`.
|
||||
/// New background_fetch() should not be started if this
|
||||
/// contains `Some`.
|
||||
background_fetch_interrupt_sender: Arc<parking_lot::Mutex<Option<Sender<()>>>>,
|
||||
}
|
||||
|
||||
impl Accounts {
|
||||
@@ -96,6 +107,7 @@ impl Accounts {
|
||||
events,
|
||||
stockstrings,
|
||||
push_subscriber,
|
||||
background_fetch_interrupt_sender: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -352,6 +364,11 @@ impl Accounts {
|
||||
///
|
||||
/// This is an auxiliary function and not part of public API.
|
||||
/// Use [Accounts::background_fetch] instead.
|
||||
///
|
||||
/// This function is cancellation-safe.
|
||||
/// It is intended to be cancellable,
|
||||
/// either because of the timeout or because background
|
||||
/// fetch was explicitly cancelled.
|
||||
async fn background_fetch_no_timeout(accounts: Vec<Context>, events: Events) {
|
||||
let n_accounts = accounts.len();
|
||||
events.emit(Event {
|
||||
@@ -378,14 +395,33 @@ impl Accounts {
|
||||
}
|
||||
|
||||
/// Auxiliary function for [Accounts::background_fetch].
|
||||
///
|
||||
/// Runs `background_fetch` until it finishes
|
||||
/// or until the timeout.
|
||||
///
|
||||
/// Produces `AccountsBackgroundFetchDone` event in every case
|
||||
/// and clears [`Self::background_fetch_interrupt_sender`]
|
||||
/// so a new background fetch can be started.
|
||||
///
|
||||
/// This function is not cancellation-safe.
|
||||
/// Cancelling it before it returns may result
|
||||
/// in not being able to run any new background fetch
|
||||
/// if interrupt sender was not cleared.
|
||||
async fn background_fetch_with_timeout(
|
||||
accounts: Vec<Context>,
|
||||
events: Events,
|
||||
timeout: std::time::Duration,
|
||||
interrupt_sender: Arc<parking_lot::Mutex<Option<Sender<()>>>>,
|
||||
interrupt_receiver: Option<Receiver<()>>,
|
||||
) {
|
||||
let Some(interrupt_receiver) = interrupt_receiver else {
|
||||
// Nothing to do if we got no interrupt receiver.
|
||||
return;
|
||||
};
|
||||
if let Err(_err) = tokio::time::timeout(
|
||||
timeout,
|
||||
Self::background_fetch_no_timeout(accounts, events.clone()),
|
||||
Self::background_fetch_no_timeout(accounts, events.clone())
|
||||
.race(interrupt_receiver.recv().map(|_| ())),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -398,10 +434,16 @@ impl Accounts {
|
||||
id: 0,
|
||||
typ: EventType::AccountsBackgroundFetchDone,
|
||||
});
|
||||
(*interrupt_sender.lock()) = None;
|
||||
}
|
||||
|
||||
/// Performs a background fetch for all accounts in parallel with a timeout.
|
||||
///
|
||||
/// Ongoing background fetch can also be cancelled manually
|
||||
/// by calling `stop_background_fetch()`, in which case it will
|
||||
/// return immediately even before the timeout expiration
|
||||
/// or finishing fetching.
|
||||
///
|
||||
/// The `AccountsBackgroundFetchDone` event is emitted at the end,
|
||||
/// process all events until you get this one and you can safely return to the background
|
||||
/// without forgetting to create notifications caused by timing race conditions.
|
||||
@@ -414,7 +456,39 @@ impl Accounts {
|
||||
) -> impl Future<Output = ()> + use<> {
|
||||
let accounts: Vec<Context> = self.accounts.values().cloned().collect();
|
||||
let events = self.events.clone();
|
||||
Self::background_fetch_with_timeout(accounts, events, timeout)
|
||||
let (sender, receiver) = async_channel::bounded(1);
|
||||
let receiver = {
|
||||
let mut lock = self.background_fetch_interrupt_sender.lock();
|
||||
if (*lock).is_some() {
|
||||
// Another background_fetch() is already running,
|
||||
// return immeidately.
|
||||
None
|
||||
} else {
|
||||
*lock = Some(sender);
|
||||
Some(receiver)
|
||||
}
|
||||
};
|
||||
Self::background_fetch_with_timeout(
|
||||
accounts,
|
||||
events,
|
||||
timeout,
|
||||
self.background_fetch_interrupt_sender.clone(),
|
||||
receiver,
|
||||
)
|
||||
}
|
||||
|
||||
/// Interrupts ongoing background_fetch() call,
|
||||
/// making it return early.
|
||||
///
|
||||
/// This method allows to cancel background_fetch() early,
|
||||
/// e.g. on Android, when `Service.onTimeout` is called.
|
||||
///
|
||||
/// If there is no ongoing background_fetch(), does nothing.
|
||||
pub fn stop_background_fetch(&self) {
|
||||
let mut lock = self.background_fetch_interrupt_sender.lock();
|
||||
if let Some(sender) = lock.take() {
|
||||
sender.try_send(()).ok();
|
||||
}
|
||||
}
|
||||
|
||||
/// Emits a single event.
|
||||
@@ -604,13 +678,12 @@ impl Config {
|
||||
// Convert them to relative paths.
|
||||
let mut modified = false;
|
||||
for account in &mut config.inner.accounts {
|
||||
if account.dir.is_absolute() {
|
||||
if let Some(old_path_parent) = account.dir.parent() {
|
||||
if let Ok(new_path) = account.dir.strip_prefix(old_path_parent) {
|
||||
account.dir = new_path.to_path_buf();
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
if account.dir.is_absolute()
|
||||
&& let Some(old_path_parent) = account.dir.parent()
|
||||
&& let Ok(new_path) = account.dir.strip_prefix(old_path_parent)
|
||||
{
|
||||
account.dir = new_path.to_path_buf();
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
if modified && writable {
|
||||
|
||||
@@ -61,9 +61,11 @@ impl fmt::Display for Aheader {
|
||||
if self.prefer_encrypt == EncryptPreference::Mutual {
|
||||
write!(fmt, " prefer-encrypt=mutual;")?;
|
||||
}
|
||||
if self.verified {
|
||||
write!(fmt, " _verified=1;")?;
|
||||
}
|
||||
// TODO After we reset all existing verifications,
|
||||
// we want to start sending the _verified attribute
|
||||
// if self.verified {
|
||||
// write!(fmt, " _verified=1;")?;
|
||||
// }
|
||||
|
||||
// adds a whitespace every 78 characters, this allows
|
||||
// email crate to wrap the lines according to RFC 5322
|
||||
@@ -282,8 +284,9 @@ mod tests {
|
||||
.contains("test@example.com")
|
||||
);
|
||||
|
||||
// We don't send the _verified header yet:
|
||||
assert!(
|
||||
format!(
|
||||
!format!(
|
||||
"{}",
|
||||
Aheader {
|
||||
addr: "test@example.com".to_string(),
|
||||
|
||||
@@ -468,7 +468,7 @@ Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@
|
||||
// The ordering in which the emails are received can matter;
|
||||
// the test _should_ pass for every ordering.
|
||||
dir.sort_by_key(|d| d.file_name());
|
||||
//rand::seq::SliceRandom::shuffle(&mut dir[..], &mut rand::thread_rng());
|
||||
//rand::seq::SliceRandom::shuffle(&mut dir[..], &mut rand::rng());
|
||||
|
||||
for entry in &dir {
|
||||
let mut file = fs::File::open(entry.path()).await?;
|
||||
|
||||
13
src/blob.rs
13
src/blob.rs
@@ -20,7 +20,7 @@ use crate::config::Config;
|
||||
use crate::constants::{self, MediaQuality};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::{LogExt, error, info, warn};
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::message::Viewtype;
|
||||
use crate::tools::sanitize_filename;
|
||||
|
||||
@@ -234,8 +234,13 @@ impl<'a> BlobObject<'a> {
|
||||
/// If `data` represents an image of known format, this adds the corresponding extension.
|
||||
///
|
||||
/// Even though this function is not async, it's OK to call it from an async context.
|
||||
pub(crate) fn store_from_base64(context: &Context, data: &str) -> Result<String> {
|
||||
let buf = base64::engine::general_purpose::STANDARD.decode(data)?;
|
||||
///
|
||||
/// Returns an error if there is an I/O problem,
|
||||
/// but in case of a failure to decode base64 returns `Ok(None)`.
|
||||
pub(crate) fn store_from_base64(context: &Context, data: &str) -> Result<Option<String>> {
|
||||
let Ok(buf) = base64::engine::general_purpose::STANDARD.decode(data) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let name = if let Ok(format) = image::guess_format(&buf) {
|
||||
if let Some(ext) = format.extensions_str().first() {
|
||||
format!("file.{ext}")
|
||||
@@ -246,7 +251,7 @@ impl<'a> BlobObject<'a> {
|
||||
String::new()
|
||||
};
|
||||
let blob = BlobObject::create_and_deduplicate_from_bytes(context, &buf, &name)?;
|
||||
Ok(blob.as_name().to_string())
|
||||
Ok(Some(blob.as_name().to_string()))
|
||||
}
|
||||
|
||||
/// Recode image to avatar size.
|
||||
|
||||
32
src/calls.rs
32
src/calls.rs
@@ -2,13 +2,14 @@
|
||||
//!
|
||||
//! Internally, calls are bound a user-visible message initializing the call.
|
||||
//! This means, the "Call ID" is a "Message ID" - similar to Webxdc IDs.
|
||||
use crate::chat::ChatIdBlocked;
|
||||
use crate::chat::{Chat, ChatId, send_msg};
|
||||
use crate::constants::Chattype;
|
||||
use crate::constants::{Blocked, Chattype};
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::log::{info, warn};
|
||||
use crate::log::warn;
|
||||
use crate::message::{self, Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::net::dns::lookup_host_with_cache;
|
||||
@@ -345,12 +346,27 @@ impl Context {
|
||||
false
|
||||
}
|
||||
};
|
||||
self.emit_event(EventType::IncomingCall {
|
||||
msg_id: call.msg.id,
|
||||
chat_id: call.msg.chat_id,
|
||||
place_call_info: call.place_call_info.to_string(),
|
||||
has_video,
|
||||
});
|
||||
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.
|
||||
}
|
||||
}
|
||||
}
|
||||
let wait = call.remaining_ring_seconds();
|
||||
task::spawn(Context::emit_end_call_if_unaccepted(
|
||||
self.clone(),
|
||||
|
||||
@@ -45,6 +45,12 @@ async fn setup_call() -> Result<CallSetup> {
|
||||
// Alice creates a chat with Bob and places an outgoing call there.
|
||||
// Alice's other device sees the same message as an outgoing call.
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
|
||||
// Create chat on Bob's side
|
||||
// so incoming call causes a notification.
|
||||
bob.create_chat(&alice).await;
|
||||
bob2.create_chat(&alice).await;
|
||||
|
||||
let test_msg_id = alice
|
||||
.place_outgoing_call(alice_chat.id, PLACE_INFO.to_string())
|
||||
.await?;
|
||||
|
||||
917
src/chat.rs
917
src/chat.rs
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -107,11 +107,6 @@ impl Chatlist {
|
||||
Ok((chat_id, msg_id))
|
||||
};
|
||||
|
||||
let process_rows = |rows: rusqlite::MappedRows<_>| {
|
||||
rows.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
};
|
||||
|
||||
let skip_id = if flag_for_forwarding {
|
||||
ChatId::lookup_by_contact(context, ContactId::DEVICE)
|
||||
.await?
|
||||
@@ -132,7 +127,7 @@ impl Chatlist {
|
||||
// groups. Otherwise it would be hard to follow conversations.
|
||||
let ids = if let Some(query_contact_id) = query_contact_id {
|
||||
// show chats shared with a given contact
|
||||
context.sql.query_map(
|
||||
context.sql.query_map_vec(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
@@ -150,7 +145,6 @@ impl Chatlist {
|
||||
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
(MessageState::OutDraft, query_contact_id, ChatVisibility::Pinned),
|
||||
process_row,
|
||||
process_rows,
|
||||
).await?
|
||||
} else if flag_archived_only {
|
||||
// show archived chats
|
||||
@@ -159,7 +153,7 @@ impl Chatlist {
|
||||
// and adapting the number requires larger refactorings and seems not to be worth the effort)
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
.query_map_vec(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
@@ -177,7 +171,6 @@ impl Chatlist {
|
||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
(MessageState::OutDraft,),
|
||||
process_row,
|
||||
process_rows,
|
||||
)
|
||||
.await?
|
||||
} else if let Some(query) = query {
|
||||
@@ -195,7 +188,7 @@ impl Chatlist {
|
||||
let str_like_cmd = format!("%{query}%");
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
.query_map_vec(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
@@ -214,7 +207,6 @@ impl Chatlist {
|
||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
(MessageState::OutDraft, skip_id, str_like_cmd, only_unread, MessageState::InFresh),
|
||||
process_row,
|
||||
process_rows,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
@@ -229,7 +221,7 @@ impl Chatlist {
|
||||
let msg_id: Option<MsgId> = row.get(3)?;
|
||||
Ok((chat_id, typ, param, msg_id))
|
||||
};
|
||||
let process_rows = |rows: rusqlite::MappedRows<_>| {
|
||||
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
|
||||
@@ -243,7 +235,6 @@ impl Chatlist {
|
||||
Err(e) => Some(Err(e)),
|
||||
})
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
};
|
||||
context.sql.query_map(
|
||||
"SELECT c.id, c.type, c.param, m.id
|
||||
@@ -272,7 +263,7 @@ impl Chatlist {
|
||||
).await?
|
||||
} else {
|
||||
// show normal chatlist
|
||||
context.sql.query_map(
|
||||
context.sql.query_map_vec(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
@@ -290,7 +281,6 @@ impl Chatlist {
|
||||
ORDER BY c.id=0 DESC, c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
(MessageState::OutDraft, skip_id, ChatVisibility::Archived, ChatVisibility::Pinned),
|
||||
process_row,
|
||||
process_rows,
|
||||
).await?
|
||||
};
|
||||
if !flag_no_specials && get_archived_cnt(context).await? > 0 {
|
||||
@@ -481,8 +471,8 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::chat::save_msgs;
|
||||
use crate::chat::{
|
||||
ProtectionStatus, add_contact_to_chat, create_group_chat, get_chat_contacts,
|
||||
remove_contact_from_chat, send_text_msg,
|
||||
add_contact_to_chat, create_group, get_chat_contacts, remove_contact_from_chat,
|
||||
send_text_msg,
|
||||
};
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::stock_str::StockMessage;
|
||||
@@ -495,15 +485,9 @@ mod tests {
|
||||
async fn test_try_load() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let bob = &tcm.bob().await;
|
||||
let chat_id1 = create_group_chat(bob, ProtectionStatus::Unprotected, "a chat")
|
||||
.await
|
||||
.unwrap();
|
||||
let chat_id2 = create_group_chat(bob, ProtectionStatus::Unprotected, "b chat")
|
||||
.await
|
||||
.unwrap();
|
||||
let chat_id3 = create_group_chat(bob, ProtectionStatus::Unprotected, "c chat")
|
||||
.await
|
||||
.unwrap();
|
||||
let chat_id1 = create_group(bob, "a chat").await.unwrap();
|
||||
let chat_id2 = create_group(bob, "b chat").await.unwrap();
|
||||
let chat_id3 = create_group(bob, "c chat").await.unwrap();
|
||||
|
||||
// check that the chatlist starts with the most recent message
|
||||
let chats = Chatlist::try_load(bob, 0, None, None).await.unwrap();
|
||||
@@ -536,9 +520,7 @@ mod tests {
|
||||
|
||||
// receive a message from alice
|
||||
let alice = &tcm.alice().await;
|
||||
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "alice chat")
|
||||
.await
|
||||
.unwrap();
|
||||
let alice_chat_id = create_group(alice, "alice chat").await.unwrap();
|
||||
add_contact_to_chat(
|
||||
alice,
|
||||
alice_chat_id,
|
||||
@@ -576,9 +558,7 @@ mod tests {
|
||||
async fn test_sort_self_talk_up_on_forward() {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.update_device_chats().await.unwrap();
|
||||
create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
|
||||
.await
|
||||
.unwrap();
|
||||
create_group(&t, "a chat").await.unwrap();
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 3);
|
||||
@@ -765,9 +745,7 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_summary_unwrap() {
|
||||
let t = TestContext::new().await;
|
||||
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
|
||||
.await
|
||||
.unwrap();
|
||||
let chat_id1 = create_group(&t, "a chat").await.unwrap();
|
||||
|
||||
let mut msg = Message::new_text("foo:\nbar \r\n test".to_string());
|
||||
chat_id1.set_draft(&t, Some(&mut msg)).await.unwrap();
|
||||
@@ -783,9 +761,7 @@ mod tests {
|
||||
async fn test_get_summary_deleted_draft() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
|
||||
.await
|
||||
.unwrap();
|
||||
let chat_id = create_group(&t, "a chat").await.unwrap();
|
||||
let mut msg = Message::new_text("Foobar".to_string());
|
||||
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
|
||||
|
||||
@@ -824,15 +800,9 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_load_broken() {
|
||||
let t = TestContext::new_bob().await;
|
||||
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
|
||||
.await
|
||||
.unwrap();
|
||||
create_group_chat(&t, ProtectionStatus::Unprotected, "b chat")
|
||||
.await
|
||||
.unwrap();
|
||||
create_group_chat(&t, ProtectionStatus::Unprotected, "c chat")
|
||||
.await
|
||||
.unwrap();
|
||||
let chat_id1 = create_group(&t, "a chat").await.unwrap();
|
||||
create_group(&t, "b chat").await.unwrap();
|
||||
create_group(&t, "c chat").await.unwrap();
|
||||
|
||||
// check that the chatlist starts with the most recent message
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
|
||||
@@ -14,15 +14,15 @@ use tokio::fs;
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::configure::EnteredLoginParam;
|
||||
use crate::constants;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::{LogExt, info};
|
||||
use crate::login_param::ConfiguredLoginParam;
|
||||
use crate::log::LogExt;
|
||||
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
|
||||
use crate::provider::{Provider, get_provider_by_id};
|
||||
use crate::sync::{self, Sync::*, SyncData};
|
||||
use crate::tools::get_abs_path;
|
||||
use crate::transport::ConfiguredLoginParam;
|
||||
use crate::{constants, stats};
|
||||
|
||||
/// The available configuration keys.
|
||||
#[derive(
|
||||
@@ -144,11 +144,11 @@ pub enum Config {
|
||||
|
||||
/// Send BCC copy to self.
|
||||
///
|
||||
/// Should be enabled for multidevice setups.
|
||||
/// Default is 0 for chatmail accounts, 1 otherwise.
|
||||
/// Should be enabled for multi-device setups.
|
||||
///
|
||||
/// This is automatically enabled when importing/exporting a backup,
|
||||
/// setting up a second device, or receiving a sync message.
|
||||
#[strum(props(default = "0"))]
|
||||
BccSelf,
|
||||
|
||||
/// True if Message Delivery Notifications (read receipts) should
|
||||
@@ -156,10 +156,6 @@ pub enum Config {
|
||||
#[strum(props(default = "1"))]
|
||||
MdnsEnabled,
|
||||
|
||||
/// True if "Sent" folder should be watched for changes.
|
||||
#[strum(props(default = "0"))]
|
||||
SentboxWatch,
|
||||
|
||||
/// True if chat messages should be moved to a separate folder. Auto-sent messages like sync
|
||||
/// ones are moved there anyway.
|
||||
#[strum(props(default = "1"))]
|
||||
@@ -285,9 +281,6 @@ pub enum Config {
|
||||
/// Configured folder for chat messages.
|
||||
ConfiguredMvboxFolder,
|
||||
|
||||
/// Configured "Sent" folder.
|
||||
ConfiguredSentboxFolder,
|
||||
|
||||
/// Configured "Trash" folder.
|
||||
ConfiguredTrashFolder,
|
||||
|
||||
@@ -389,12 +382,6 @@ pub enum Config {
|
||||
/// Make all outgoing messages with Autocrypt header "multipart/signed".
|
||||
SignUnencrypted,
|
||||
|
||||
/// Enable header protection for `Autocrypt` header.
|
||||
///
|
||||
/// This is an experimental setting not compatible to other MUAs
|
||||
/// and older Delta Chat versions (core version <= v1.149.0).
|
||||
ProtectAutocrypt,
|
||||
|
||||
/// Let the core save all events to the database.
|
||||
/// This value is used internally to remember the MsgId of the logging xdc
|
||||
#[strum(props(default = "0"))]
|
||||
@@ -414,9 +401,22 @@ pub enum Config {
|
||||
/// used for signatures, encryption to self and included in `Autocrypt` header.
|
||||
KeyId,
|
||||
|
||||
/// This key is sent to the self_reporting bot so that the bot can recognize the user
|
||||
/// Send statistics to Delta Chat's developers.
|
||||
/// Can be exposed to the user as a setting.
|
||||
StatsSending,
|
||||
|
||||
/// Last time statistics were sent to Delta Chat's developers
|
||||
StatsLastSent,
|
||||
|
||||
/// Last time `update_message_stats()` was called
|
||||
StatsLastUpdate,
|
||||
|
||||
/// This key is sent to the statistics bot so that the bot can recognize the user
|
||||
/// without storing the email address
|
||||
SelfReportingId,
|
||||
StatsId,
|
||||
|
||||
/// The last contact id that already existed when statistics-sending was enabled for the first time.
|
||||
StatsLastOldContactId,
|
||||
|
||||
/// MsgId of webxdc map integration.
|
||||
WebxdcIntegration,
|
||||
@@ -438,8 +438,19 @@ pub enum Config {
|
||||
/// storing the same token multiple times on the server.
|
||||
EncryptedDeviceToken,
|
||||
|
||||
/// Enables running test hooks, e.g. see `InnerContext::pre_encrypt_mime_hook`.
|
||||
/// This way is better than conditional compilation, i.e. `#[cfg(test)]`, because tests not
|
||||
/// using this still run unmodified code.
|
||||
TestHooks,
|
||||
|
||||
/// Return an error from `receive_imf_inner()` for a fully downloaded message. For tests.
|
||||
FailOnReceivingFullMsg,
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -466,10 +477,7 @@ impl Config {
|
||||
|
||||
/// Whether the config option needs an IO scheduler restart to take effect.
|
||||
pub(crate) fn needs_io_restart(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Config::MvboxMove | Config::OnlyFetchMvbox | Config::SentboxWatch
|
||||
)
|
||||
matches!(self, Config::MvboxMove | Config::OnlyFetchMvbox)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -515,10 +523,6 @@ impl Context {
|
||||
|
||||
// Default values
|
||||
let val = match key {
|
||||
Config::BccSelf => match Box::pin(self.is_chatmail()).await? {
|
||||
false => Some("1".to_string()),
|
||||
true => Some("0".to_string()),
|
||||
},
|
||||
Config::ConfiguredInboxFolder => Some("INBOX".to_string()),
|
||||
Config::DeleteServerAfter => {
|
||||
match !Box::pin(self.get_config_bool(Config::BccSelf)).await?
|
||||
@@ -582,8 +586,9 @@ impl Context {
|
||||
/// Returns boolean configuration value for the given key.
|
||||
pub async fn get_config_bool(&self, key: Config) -> Result<bool> {
|
||||
Ok(self
|
||||
.get_config_parsed::<i32>(key)
|
||||
.get_config(key)
|
||||
.await?
|
||||
.and_then(|s| s.parse::<i32>().ok())
|
||||
.map(|x| x != 0)
|
||||
.unwrap_or_default())
|
||||
}
|
||||
@@ -595,15 +600,6 @@ impl Context {
|
||||
|| !self.get_config_bool(Config::IsChatmail).await?)
|
||||
}
|
||||
|
||||
/// Returns true if sentbox ("Sent" folder) should be watched.
|
||||
pub(crate) async fn should_watch_sentbox(&self) -> Result<bool> {
|
||||
Ok(self.get_config_bool(Config::SentboxWatch).await?
|
||||
&& self
|
||||
.get_config(Config::ConfiguredSentboxFolder)
|
||||
.await?
|
||||
.is_some())
|
||||
}
|
||||
|
||||
/// Returns true if sync messages should be sent.
|
||||
pub(crate) async fn should_send_sync_msgs(&self) -> Result<bool> {
|
||||
Ok(self.get_config_bool(Config::SyncMsgs).await?
|
||||
@@ -676,7 +672,7 @@ impl Context {
|
||||
Config::Selfavatar if value.is_empty() => None,
|
||||
Config::Selfavatar => {
|
||||
config_value = BlobObject::store_from_base64(self, value)?;
|
||||
Some(config_value.as_str())
|
||||
config_value.as_deref()
|
||||
}
|
||||
_ => Some(value),
|
||||
};
|
||||
@@ -692,7 +688,6 @@ impl Context {
|
||||
| Config::ProxyEnabled
|
||||
| Config::BccSelf
|
||||
| Config::MdnsEnabled
|
||||
| Config::SentboxWatch
|
||||
| Config::MvboxMove
|
||||
| Config::OnlyFetchMvbox
|
||||
| Config::DeleteToTrash
|
||||
@@ -722,9 +717,15 @@ impl Context {
|
||||
true => self.scheduler.pause(self).await?,
|
||||
_ => Default::default(),
|
||||
};
|
||||
if key == Config::StatsSending {
|
||||
let old_value = self.get_config(key).await?;
|
||||
let old_value = bool_from_config(old_value.as_deref());
|
||||
let new_value = bool_from_config(value);
|
||||
stats::pre_sending_config_change(self, old_value, new_value).await?;
|
||||
}
|
||||
self.set_config_internal(key, value).await?;
|
||||
if key == Config::SentboxWatch {
|
||||
self.last_full_folder_scan.lock().await.take();
|
||||
if key == Config::StatsSending {
|
||||
stats::maybe_send_stats(self).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -877,6 +878,10 @@ pub(crate) fn from_bool(val: bool) -> Option<&'static str> {
|
||||
Some(if val { "1" } else { "0" })
|
||||
}
|
||||
|
||||
pub(crate) fn bool_from_config(config: Option<&str>) -> bool {
|
||||
config.is_some_and(|v| v.parse::<i32>().unwrap_or_default() != 0)
|
||||
}
|
||||
|
||||
// Separate impl block for self address handling
|
||||
impl Context {
|
||||
/// Determine whether the specified addr maps to the/a self addr.
|
||||
|
||||
@@ -27,18 +27,21 @@ use crate::config::{self, Config};
|
||||
use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT;
|
||||
use crate::context::Context;
|
||||
use crate::imap::Imap;
|
||||
use crate::log::{LogExt, info, warn};
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::login_param::EnteredCertificateChecks;
|
||||
pub use crate::login_param::EnteredLoginParam;
|
||||
use crate::login_param::{
|
||||
ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
|
||||
ConnectionCandidate, EnteredCertificateChecks, ProxyConfig,
|
||||
};
|
||||
use crate::message::Message;
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::oauth2::get_oauth2_addr;
|
||||
use crate::provider::{Protocol, Provider, Socket, UsernamePattern};
|
||||
use crate::qr::{login_param_from_account_qr, login_param_from_login_qr};
|
||||
use crate::smtp::Smtp;
|
||||
use crate::sync::Sync::*;
|
||||
use crate::tools::time;
|
||||
use crate::transport::{
|
||||
ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
|
||||
ConnectionCandidate,
|
||||
};
|
||||
use crate::{EventType, stock_str};
|
||||
use crate::{chat, provider};
|
||||
use deltachat_contact_tools::addr_cmp;
|
||||
@@ -117,7 +120,7 @@ impl Context {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_transport_inner(&self, param: &mut EnteredLoginParam) -> Result<()> {
|
||||
pub(crate) async fn add_transport_inner(&self, param: &mut EnteredLoginParam) -> Result<()> {
|
||||
ensure!(
|
||||
!self.scheduler.is_running().await,
|
||||
"cannot configure, already running"
|
||||
@@ -162,20 +165,15 @@ impl Context {
|
||||
pub async fn add_transport_from_qr(&self, qr: &str) -> Result<()> {
|
||||
self.stop_io().await;
|
||||
|
||||
// This code first sets the deprecated Config::Addr, Config::MailPw, etc.
|
||||
// and then calls configure(), which loads them again.
|
||||
// At some point, we will remove configure()
|
||||
// and then simplify the code
|
||||
// to directly create an EnteredLoginParam.
|
||||
let result = async move {
|
||||
match crate::qr::check_qr(self, qr).await? {
|
||||
crate::qr::Qr::Account { .. } => crate::qr::set_account_from_qr(self, qr).await?,
|
||||
let mut param = match crate::qr::check_qr(self, qr).await? {
|
||||
crate::qr::Qr::Account { .. } => login_param_from_account_qr(self, qr).await?,
|
||||
crate::qr::Qr::Login { address, options } => {
|
||||
crate::qr::configure_from_login_qr(self, &address, options).await?
|
||||
login_param_from_login_qr(&address, options)?
|
||||
}
|
||||
_ => bail!("QR code does not contain account"),
|
||||
}
|
||||
self.configure().await?;
|
||||
};
|
||||
self.add_transport_inner(&mut param).await?;
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
@@ -196,16 +194,11 @@ impl Context {
|
||||
pub async fn list_transports(&self) -> Result<Vec<EnteredLoginParam>> {
|
||||
let transports = self
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT entered_param FROM transports",
|
||||
(),
|
||||
|row| row.get::<_, String>(0),
|
||||
|rows| {
|
||||
rows.flatten()
|
||||
.map(|s| Ok(serde_json::from_str(&s)?))
|
||||
.collect::<Result<Vec<EnteredLoginParam>>>()
|
||||
},
|
||||
)
|
||||
.query_map_vec("SELECT entered_param FROM transports", (), |row| {
|
||||
let entered_param: String = row.get(0)?;
|
||||
let transport: EnteredLoginParam = serde_json::from_str(&entered_param)?;
|
||||
Ok(transport)
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(transports)
|
||||
@@ -265,19 +258,18 @@ async fn on_configure_completed(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(new_addr) = context.get_config(Config::ConfiguredAddr).await? {
|
||||
if let Some(old_addr) = old_addr {
|
||||
if !addr_cmp(&new_addr, &old_addr) {
|
||||
let mut msg = Message::new_text(
|
||||
stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await,
|
||||
);
|
||||
chat::add_device_msg(context, None, Some(&mut msg))
|
||||
.await
|
||||
.context("Cannot add AEAP explanation")
|
||||
.log_err(context)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
if let Some(new_addr) = context.get_config(Config::ConfiguredAddr).await?
|
||||
&& let Some(old_addr) = old_addr
|
||||
&& !addr_cmp(&new_addr, &old_addr)
|
||||
{
|
||||
let mut msg = Message::new_text(
|
||||
stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await,
|
||||
);
|
||||
chat::add_device_msg(context, None, Some(&mut msg))
|
||||
.await
|
||||
.context("Cannot add AEAP explanation")
|
||||
.log_err(context)
|
||||
.ok();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -300,8 +292,6 @@ async fn get_configured_param(
|
||||
param.smtp.password.clone()
|
||||
};
|
||||
|
||||
let proxy_enabled = ctx.get_config_bool(Config::ProxyEnabled).await?;
|
||||
|
||||
let mut addr = param.addr.clone();
|
||||
if param.oauth2 {
|
||||
// the used oauth2 addr may differ, check this.
|
||||
@@ -343,7 +333,7 @@ async fn get_configured_param(
|
||||
"checking internal provider-info for offline autoconfig"
|
||||
);
|
||||
|
||||
provider = provider::get_provider_info(ctx, ¶m_domain, proxy_enabled).await;
|
||||
provider = provider::get_provider_info(¶m_domain);
|
||||
if let Some(provider) = provider {
|
||||
if provider.server.is_empty() {
|
||||
info!(ctx, "Offline autoconfig found, but no servers defined.");
|
||||
@@ -555,7 +545,6 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
|
||||
true => ctx.get_config_bool(Config::IsChatmail).await?,
|
||||
};
|
||||
if is_chatmail {
|
||||
ctx.set_config(Config::SentboxWatch, None).await?;
|
||||
ctx.set_config(Config::MvboxMove, Some("0")).await?;
|
||||
ctx.set_config(Config::OnlyFetchMvbox, None).await?;
|
||||
ctx.set_config(Config::ShowEmails, None).await?;
|
||||
@@ -575,14 +564,6 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
|
||||
|
||||
progress!(ctx, 910);
|
||||
|
||||
if let Some(configured_addr) = ctx.get_config(Config::ConfiguredAddr).await? {
|
||||
if configured_addr != param.addr {
|
||||
// Switched account, all server UIDs we know are invalid
|
||||
info!(ctx, "Scheduling resync because the address has changed.");
|
||||
ctx.schedule_resync().await?;
|
||||
}
|
||||
}
|
||||
|
||||
let provider = configured_param.provider;
|
||||
configured_param
|
||||
.save_to_transports_table(ctx, param)
|
||||
|
||||
@@ -28,8 +28,9 @@ struct MozAutoconfigure {
|
||||
pub outgoing_servers: Vec<Server>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Default)]
|
||||
enum MozConfigTag {
|
||||
#[default]
|
||||
Undefined,
|
||||
Hostname,
|
||||
Port,
|
||||
@@ -37,12 +38,6 @@ enum MozConfigTag {
|
||||
Username,
|
||||
}
|
||||
|
||||
impl Default for MozConfigTag {
|
||||
fn default() -> Self {
|
||||
Self::Undefined
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for MozConfigTag {
|
||||
type Err = ();
|
||||
|
||||
@@ -159,10 +154,10 @@ fn parse_xml_reader<B: BufRead>(
|
||||
if let Some(incoming_server) = parse_server(reader, event)? {
|
||||
incoming_servers.push(incoming_server);
|
||||
}
|
||||
} else if tag == "outgoingserver" {
|
||||
if let Some(outgoing_server) = parse_server(reader, event)? {
|
||||
outgoing_servers.push(outgoing_server);
|
||||
}
|
||||
} else if tag == "outgoingserver"
|
||||
&& let Some(outgoing_server) = parse_server(reader, event)?
|
||||
{
|
||||
outgoing_servers.push(outgoing_server);
|
||||
}
|
||||
}
|
||||
Event::Eof => break,
|
||||
|
||||
@@ -101,6 +101,8 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
FromPrimitive,
|
||||
ToPrimitive,
|
||||
FromSql,
|
||||
@@ -118,7 +120,7 @@ pub enum Chattype {
|
||||
|
||||
/// Group chat.
|
||||
///
|
||||
/// Created by [`crate::chat::create_group_chat`].
|
||||
/// Created by [`crate::chat::create_group`].
|
||||
Group = 120,
|
||||
|
||||
/// An (unencrypted) mailing list,
|
||||
@@ -221,6 +223,9 @@ pub(crate) const DC_FOLDERS_CONFIGURED_VERSION: i32 = 5;
|
||||
// `max_smtp_rcpt_to` in the provider db.
|
||||
pub(crate) const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;
|
||||
|
||||
/// Same as `DEFAULT_MAX_SMTP_RCPT_TO`, but for chatmail relays.
|
||||
pub(crate) const DEFAULT_CHATMAIL_MAX_SMTP_RCPT_TO: usize = 999;
|
||||
|
||||
/// How far the last quota check needs to be in the past to be checked by the background function (in seconds).
|
||||
pub(crate) const DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT: u64 = 12 * 60 * 60; // 12 hours
|
||||
|
||||
@@ -248,6 +253,18 @@ pub(crate) const ASM_BODY: &str = "This is the Autocrypt Setup Message \
|
||||
/// Period between `sql::housekeeping()` runs.
|
||||
pub(crate) const HOUSEKEEPING_PERIOD: i64 = 24 * 60 * 60;
|
||||
|
||||
pub(crate) const BROADCAST_INCOMPATIBILITY_MSG: &str = r#"The up to now "experimental channels feature" is about to become an officially supported one. By that, privacy will be improved, it will become faster, and less traffic will be consumed.
|
||||
|
||||
As we do not guarantee feature-stability for such experiments, this means, that you will need to create the channel again.
|
||||
|
||||
Here is what to do:
|
||||
• Create a new channel
|
||||
• Tap on the channel name
|
||||
• Tap on "QR Invite Code"
|
||||
• Have all recipients scan the QR code, or send them the link
|
||||
|
||||
If you have any questions, please send an email to delta@merlinux.eu or ask at https://support.delta.chat/."#;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
118
src/contact.rs
118
src/contact.rs
@@ -31,7 +31,7 @@ use crate::key::{
|
||||
DcKey, Fingerprint, SignedPublicKey, load_self_public_key, self_fingerprint,
|
||||
self_fingerprint_opt,
|
||||
};
|
||||
use crate::log::{LogExt, info, warn};
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::message::MessageState;
|
||||
use crate::mimeparser::AvatarAction;
|
||||
use crate::param::{Param, Params};
|
||||
@@ -138,27 +138,27 @@ impl ContactId {
|
||||
})
|
||||
.await?;
|
||||
|
||||
if sync.into() {
|
||||
if let Some((addr, fingerprint)) = row {
|
||||
if fingerprint.is_empty() {
|
||||
chat::sync(
|
||||
context,
|
||||
chat::SyncId::ContactAddr(addr),
|
||||
chat::SyncAction::Rename(name.to_string()),
|
||||
)
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
} else {
|
||||
chat::sync(
|
||||
context,
|
||||
chat::SyncId::ContactFingerprint(fingerprint),
|
||||
chat::SyncAction::Rename(name.to_string()),
|
||||
)
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
}
|
||||
if sync.into()
|
||||
&& let Some((addr, fingerprint)) = row
|
||||
{
|
||||
if fingerprint.is_empty() {
|
||||
chat::sync(
|
||||
context,
|
||||
chat::SyncId::ContactAddr(addr),
|
||||
chat::SyncAction::Rename(name.to_string()),
|
||||
)
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
} else {
|
||||
chat::sync(
|
||||
context,
|
||||
chat::SyncId::ContactFingerprint(fingerprint),
|
||||
chat::SyncAction::Rename(name.to_string()),
|
||||
)
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -369,16 +369,15 @@ async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Resu
|
||||
return Ok(id);
|
||||
}
|
||||
let path = match &contact.profile_image {
|
||||
Some(image) => match BlobObject::store_from_base64(context, image) {
|
||||
Err(e) => {
|
||||
Some(image) => match BlobObject::store_from_base64(context, image)? {
|
||||
None => {
|
||||
warn!(
|
||||
context,
|
||||
"import_vcard_contact: Could not decode and save avatar for {}: {e:#}.",
|
||||
contact.addr
|
||||
"import_vcard_contact: Could not decode avatar for {}.", contact.addr
|
||||
);
|
||||
None
|
||||
}
|
||||
Ok(path) => Some(path),
|
||||
Some(path) => Some(path),
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
@@ -394,13 +393,13 @@ async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Resu
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(biography) = &contact.biography {
|
||||
if let Err(e) = set_status(context, id, biography.to_owned(), false, false).await {
|
||||
warn!(
|
||||
context,
|
||||
"import_vcard_contact: Could not set biography for {}: {e:#}.", contact.addr
|
||||
);
|
||||
}
|
||||
if let Some(biography) = &contact.biography
|
||||
&& let Err(e) = set_status(context, id, biography.to_owned(), false, false).await
|
||||
{
|
||||
warn!(
|
||||
context,
|
||||
"import_vcard_contact: Could not set biography for {}: {e:#}.", contact.addr
|
||||
);
|
||||
}
|
||||
Ok(id)
|
||||
}
|
||||
@@ -1282,14 +1281,13 @@ impl Contact {
|
||||
|
||||
let list = context
|
||||
.sql
|
||||
.query_map(
|
||||
.query_map_vec(
|
||||
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY last_seen DESC, id DESC;",
|
||||
(ContactId::LAST_SPECIAL,),
|
||||
|row| row.get::<_, ContactId>(0),
|
||||
|ids| {
|
||||
ids.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
|row| {
|
||||
let contact_id: ContactId = row.get(0)?;
|
||||
Ok(contact_id)
|
||||
}
|
||||
)
|
||||
.await?;
|
||||
Ok(list)
|
||||
@@ -1566,20 +1564,33 @@ impl Contact {
|
||||
if show_fallback_icon && !self.id.is_special() && !self.is_key_contact() {
|
||||
return Ok(Some(chat::get_unencrypted_icon(context).await?));
|
||||
}
|
||||
if let Some(image_rel) = self.param.get(Param::ProfileImage) {
|
||||
if !image_rel.is_empty() {
|
||||
return Ok(Some(get_abs_path(context, Path::new(image_rel))));
|
||||
}
|
||||
if let Some(image_rel) = self.param.get(Param::ProfileImage)
|
||||
&& !image_rel.is_empty()
|
||||
{
|
||||
return Ok(Some(get_abs_path(context, Path::new(image_rel))));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Returns a color for the contact.
|
||||
/// See [`self::get_color`].
|
||||
/// For self-contact this returns gray if own keypair doesn't exist yet.
|
||||
/// See also [`self::get_color`].
|
||||
pub fn get_color(&self) -> u32 {
|
||||
get_color(self.id == ContactId::SELF, &self.addr, &self.fingerprint())
|
||||
}
|
||||
|
||||
/// Returns a color for the contact.
|
||||
/// Ensures that the color isn't gray. For self-contact this generates own keypair if it doesn't
|
||||
/// exist yet.
|
||||
/// See also [`self::get_color`].
|
||||
pub async fn get_or_gen_color(&self, context: &Context) -> Result<u32> {
|
||||
let mut fpr = self.fingerprint();
|
||||
if fpr.is_none() && self.id == ContactId::SELF {
|
||||
fpr = Some(load_self_public_key(context).await?.dc_fingerprint());
|
||||
}
|
||||
Ok(get_color(self.id == ContactId::SELF, &self.addr, &fpr))
|
||||
}
|
||||
|
||||
/// Gets the contact's status.
|
||||
///
|
||||
/// Status is the last signature received in a message from this contact.
|
||||
@@ -1789,12 +1800,11 @@ WHERE type=? AND id IN (
|
||||
|
||||
// also unblock mailinglist
|
||||
// if the contact is a mailinglist address explicitly created to allow unblocking
|
||||
if !new_blocking && contact.origin == Origin::MailinglistAddress {
|
||||
if let Some((chat_id, _, _)) =
|
||||
chat::get_chat_id_by_grpid(context, &contact.addr).await?
|
||||
{
|
||||
chat_id.unblock_ex(context, Nosync).await?;
|
||||
}
|
||||
if !new_blocking
|
||||
&& contact.origin == Origin::MailinglistAddress
|
||||
&& let Some((chat_id, ..)) = chat::get_chat_id_by_grpid(context, &contact.addr).await?
|
||||
{
|
||||
chat_id.unblock_ex(context, Nosync).await?;
|
||||
}
|
||||
|
||||
if sync.into() {
|
||||
@@ -2050,7 +2060,7 @@ impl RecentlySeenLoop {
|
||||
// become unseen in the future.
|
||||
let mut unseen_queue: BinaryHeap<MyHeapElem> = context
|
||||
.sql
|
||||
.query_map(
|
||||
.query_map_collect(
|
||||
"SELECT id, last_seen FROM contacts
|
||||
WHERE last_seen > ?",
|
||||
(now_ts - SEEN_RECENTLY_SECONDS,),
|
||||
@@ -2059,10 +2069,6 @@ impl RecentlySeenLoop {
|
||||
let last_seen: i64 = row.get("last_seen")?;
|
||||
Ok((Reverse(last_seen + SEEN_RECENTLY_SECONDS), contact_id))
|
||||
},
|
||||
|rows| {
|
||||
rows.collect::<std::result::Result<BinaryHeap<MyHeapElem>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use deltachat_contact_tools::{addr_cmp, may_be_valid_addr};
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{Chat, ProtectionStatus, get_chat_contacts, send_text_msg};
|
||||
use crate::chat::{Chat, get_chat_contacts, send_text_msg};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::test_utils::{self, TestContext, TestContextManager, TimeShiftFalsePositiveNote};
|
||||
|
||||
#[test]
|
||||
@@ -775,16 +774,21 @@ async fn test_contact_get_color() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_self_color_vs_key() -> Result<()> {
|
||||
async fn test_self_color() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.unconfigured().await;
|
||||
t.configure_addr("alice@example.org").await;
|
||||
assert!(t.is_configured().await?);
|
||||
let color = Contact::get_by_id(t, ContactId::SELF).await?.get_color();
|
||||
let self_contact = Contact::get_by_id(t, ContactId::SELF).await?;
|
||||
let color = self_contact.get_color();
|
||||
assert_eq!(color, 0x808080);
|
||||
get_securejoin_qr(t, None).await?;
|
||||
let color1 = Contact::get_by_id(t, ContactId::SELF).await?.get_color();
|
||||
assert_ne!(color1, color);
|
||||
let color = self_contact.get_or_gen_color(t).await?;
|
||||
assert_ne!(color, 0x808080);
|
||||
let color1 = self_contact.get_or_gen_color(t).await?;
|
||||
assert_eq!(color1, color);
|
||||
|
||||
let bob = &tcm.bob().await;
|
||||
assert_eq!(bob.add_or_lookup_contact(t).await.get_color(), color);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1320,9 +1324,6 @@ async fn test_self_is_verified() -> Result<()> {
|
||||
assert!(contact.get_verifier_id(&alice).await?.is_none());
|
||||
assert!(contact.is_key_contact());
|
||||
|
||||
let chat_id = ChatId::get_for_contact(&alice, ContactId::SELF).await?;
|
||||
assert!(chat_id.is_protected(&alice).await.unwrap() == ProtectionStatus::Protected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
306
src/context.rs
306
src/context.rs
@@ -4,34 +4,28 @@ use std::collections::{BTreeMap, HashMap};
|
||||
use std::ffi::OsString;
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result, bail, ensure};
|
||||
use async_channel::{self as channel, Receiver, Sender};
|
||||
use pgp::types::PublicKeyTrait;
|
||||
use ratelimit::Ratelimit;
|
||||
use tokio::sync::{Mutex, Notify, RwLock};
|
||||
|
||||
use crate::chat::{ChatId, ProtectionStatus, get_chat_cnt};
|
||||
use crate::chatlist_events;
|
||||
use crate::chat::{ChatId, get_chat_cnt};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{
|
||||
self, DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_CHAT_ID_TRASH, DC_VERSION_STR,
|
||||
};
|
||||
use crate::contact::{Contact, ContactId, import_vcard, mark_contact_id_as_verified};
|
||||
use crate::constants::{self, DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_VERSION_STR};
|
||||
use crate::contact::{Contact, ContactId};
|
||||
use crate::debug_logging::DebugLogging;
|
||||
use crate::download::DownloadState;
|
||||
use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::imap::{FolderMeaning, Imap, ServerMetadata};
|
||||
use crate::key::{load_self_secret_key, self_fingerprint};
|
||||
use crate::log::{info, warn};
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::log::warn;
|
||||
use crate::logged_debug_assert;
|
||||
use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam};
|
||||
use crate::message::{self, Message, MessageState, MsgId};
|
||||
use crate::login_param::EnteredLoginParam;
|
||||
use crate::message::{self, MessageState, MsgId};
|
||||
use crate::net::tls::TlsSessionStore;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peer_channels::Iroh;
|
||||
use crate::push::PushSubscriber;
|
||||
use crate::quota::QuotaInfo;
|
||||
@@ -39,7 +33,9 @@ use crate::scheduler::{ConnectivityStore, SchedulerState, convert_folder_meaning
|
||||
use crate::sql::Sql;
|
||||
use crate::stock_str::StockStrings;
|
||||
use crate::timesmearing::SmearedTimestamp;
|
||||
use crate::tools::{self, create_id, duration_to_str, time, time_elapsed};
|
||||
use crate::tools::{self, duration_to_str, time, time_elapsed};
|
||||
use crate::transport::ConfiguredLoginParam;
|
||||
use crate::{chatlist_events, stats};
|
||||
|
||||
/// Builder for the [`Context`].
|
||||
///
|
||||
@@ -142,7 +138,7 @@ impl ContextBuilder {
|
||||
///
|
||||
/// This is useful in order to share the same translation strings in all [`Context`]s.
|
||||
/// The mapping may be empty when set, it will be populated by
|
||||
/// [`Context::set_stock-translation`] or [`Accounts::set_stock_translation`] calls.
|
||||
/// [`Context::set_stock_translation`] or [`Accounts::set_stock_translation`] calls.
|
||||
///
|
||||
/// Note that the [account manager](crate::accounts::Accounts) is designed to handle the
|
||||
/// common case for using multiple [`Context`] instances.
|
||||
@@ -247,9 +243,6 @@ pub struct InnerContext {
|
||||
/// Set to `None` if quota was never tried to load.
|
||||
pub(crate) quota: RwLock<Option<QuotaInfo>>,
|
||||
|
||||
/// IMAP UID resync request.
|
||||
pub(crate) resync_request: AtomicBool,
|
||||
|
||||
/// Notify about new messages.
|
||||
///
|
||||
/// This causes [`Context::wait_next_msgs`] to wake up.
|
||||
@@ -263,8 +256,6 @@ pub struct InnerContext {
|
||||
/// IMAP METADATA.
|
||||
pub(crate) metadata: RwLock<Option<ServerMetadata>>,
|
||||
|
||||
pub(crate) last_full_folder_scan: Mutex<Option<tools::Time>>,
|
||||
|
||||
/// ID for this `Context` in the current process.
|
||||
///
|
||||
/// This allows for multiple `Context`s open in a single process where each context can
|
||||
@@ -312,10 +303,21 @@ pub struct InnerContext {
|
||||
/// `Connectivity` values for mailboxes, unordered. Used to compute the aggregate connectivity,
|
||||
/// see [`Context::get_connectivity()`].
|
||||
pub(crate) connectivities: parking_lot::Mutex<Vec<ConnectivityStore>>,
|
||||
|
||||
#[expect(clippy::type_complexity)]
|
||||
/// Transforms the root of the cryptographic payload before encryption.
|
||||
pub(crate) pre_encrypt_mime_hook: parking_lot::Mutex<
|
||||
Option<
|
||||
for<'a> fn(
|
||||
&Context,
|
||||
mail_builder::mime::MimePart<'a>,
|
||||
) -> mail_builder::mime::MimePart<'a>,
|
||||
>,
|
||||
>,
|
||||
}
|
||||
|
||||
/// The state of ongoing process.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Default)]
|
||||
enum RunningState {
|
||||
/// Ongoing process is allocated.
|
||||
Running { cancel_sender: Sender<()> },
|
||||
@@ -324,15 +326,10 @@ enum RunningState {
|
||||
ShallStop { request: tools::Time },
|
||||
|
||||
/// There is no ongoing process, a new one can be allocated.
|
||||
#[default]
|
||||
Stopped,
|
||||
}
|
||||
|
||||
impl Default for RunningState {
|
||||
fn default() -> Self {
|
||||
Self::Stopped
|
||||
}
|
||||
}
|
||||
|
||||
/// Return some info about deltachat-core
|
||||
///
|
||||
/// This contains information mostly about the library itself, the
|
||||
@@ -468,12 +465,10 @@ impl Context {
|
||||
scheduler: SchedulerState::new(),
|
||||
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow at least 1 message every 10 seconds + a burst of 6.
|
||||
quota: RwLock::new(None),
|
||||
resync_request: AtomicBool::new(false),
|
||||
new_msgs_notify,
|
||||
server_id: RwLock::new(None),
|
||||
metadata: RwLock::new(None),
|
||||
creation_time: tools::Time::now(),
|
||||
last_full_folder_scan: Mutex::new(None),
|
||||
last_error: parking_lot::RwLock::new("".to_string()),
|
||||
migration_error: parking_lot::RwLock::new(None),
|
||||
debug_logging: std::sync::RwLock::new(None),
|
||||
@@ -483,6 +478,7 @@ impl Context {
|
||||
iroh: Arc::new(RwLock::new(None)),
|
||||
self_fingerprint: OnceLock::new(),
|
||||
connectivities: parking_lot::Mutex::new(Vec::new()),
|
||||
pre_encrypt_mime_hook: None.into(),
|
||||
};
|
||||
|
||||
let ctx = Context {
|
||||
@@ -561,7 +557,7 @@ impl Context {
|
||||
.and_then(|provider| provider.opt.max_smtp_rcpt_to)
|
||||
.map_or_else(
|
||||
|| match is_chatmail {
|
||||
true => usize::MAX,
|
||||
true => constants::DEFAULT_CHATMAIL_MAX_SMTP_RCPT_TO,
|
||||
false => constants::DEFAULT_MAX_SMTP_RCPT_TO,
|
||||
},
|
||||
usize::from,
|
||||
@@ -612,10 +608,9 @@ impl Context {
|
||||
if self
|
||||
.quota_needs_update(DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT)
|
||||
.await
|
||||
&& let Err(err) = self.update_recent_quota(&mut session).await
|
||||
{
|
||||
if let Err(err) = self.update_recent_quota(&mut session).await {
|
||||
warn!(self, "Failed to update quota: {err:#}.");
|
||||
}
|
||||
warn!(self, "Failed to update quota: {err:#}.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -628,12 +623,6 @@ impl Context {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn schedule_resync(&self) -> Result<()> {
|
||||
self.resync_request.store(true, Ordering::Relaxed);
|
||||
self.scheduler.interrupt_inbox().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a reference to the underlying SQL instance.
|
||||
///
|
||||
/// Warning: this is only here for testing, not part of the public API.
|
||||
@@ -826,7 +815,6 @@ impl Context {
|
||||
let unblocked_msgs = message::get_unblocked_msg_cnt(self).await;
|
||||
let request_msgs = message::get_request_msg_cnt(self).await;
|
||||
let contacts = Contact::get_real_cnt(self).await?;
|
||||
let is_configured = self.get_config_int(Config::Configured).await?;
|
||||
let proxy_enabled = self.get_config_int(Config::ProxyEnabled).await?;
|
||||
let dbversion = self
|
||||
.sql
|
||||
@@ -854,7 +842,6 @@ impl Context {
|
||||
Err(err) => format!("<key failure: {err}>"),
|
||||
};
|
||||
|
||||
let sentbox_watch = self.get_config_int(Config::SentboxWatch).await?;
|
||||
let mvbox_move = self.get_config_int(Config::MvboxMove).await?;
|
||||
let only_fetch_mvbox = self.get_config_int(Config::OnlyFetchMvbox).await?;
|
||||
let folders_configured = self
|
||||
@@ -867,10 +854,6 @@ impl Context {
|
||||
.get_config(Config::ConfiguredInboxFolder)
|
||||
.await?
|
||||
.unwrap_or_else(|| "<unset>".to_string());
|
||||
let configured_sentbox_folder = self
|
||||
.get_config(Config::ConfiguredSentboxFolder)
|
||||
.await?
|
||||
.unwrap_or_else(|| "<unset>".to_string());
|
||||
let configured_mvbox_folder = self
|
||||
.get_config(Config::ConfiguredMvboxFolder)
|
||||
.await?
|
||||
@@ -905,7 +888,6 @@ impl Context {
|
||||
.await?
|
||||
.unwrap_or_else(|| "<unset>".to_string()),
|
||||
);
|
||||
res.insert("is_configured", is_configured.to_string());
|
||||
res.insert("proxy_enabled", proxy_enabled.to_string());
|
||||
res.insert("entered_account_settings", l.to_string());
|
||||
res.insert("used_account_settings", l2);
|
||||
@@ -959,7 +941,6 @@ impl Context {
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert("sentbox_watch", sentbox_watch.to_string());
|
||||
res.insert("mvbox_move", mvbox_move.to_string());
|
||||
res.insert("only_fetch_mvbox", only_fetch_mvbox.to_string());
|
||||
res.insert(
|
||||
@@ -967,7 +948,6 @@ impl Context {
|
||||
folders_configured.to_string(),
|
||||
);
|
||||
res.insert("configured_inbox_folder", configured_inbox_folder);
|
||||
res.insert("configured_sentbox_folder", configured_sentbox_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());
|
||||
@@ -1035,12 +1015,6 @@ impl Context {
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"protect_autocrypt",
|
||||
self.get_config_int(Config::ProtectAutocrypt)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"debug_logging",
|
||||
self.get_config_int(Config::DebugLogging).await?.to_string(),
|
||||
@@ -1072,6 +1046,29 @@ impl Context {
|
||||
.await?
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
res.insert(
|
||||
"stats_id",
|
||||
self.get_config(Config::StatsId)
|
||||
.await?
|
||||
.unwrap_or_else(|| "<unset>".to_string()),
|
||||
);
|
||||
res.insert(
|
||||
"stats_sending",
|
||||
stats::should_send_stats(self).await?.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"stats_last_sent",
|
||||
self.get_config_i64(Config::StatsLastSent)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"test_hooks",
|
||||
self.sql
|
||||
.get_raw_config("test_hooks")
|
||||
.await?
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
res.insert(
|
||||
"fail_on_receiving_full_msg",
|
||||
self.sql
|
||||
@@ -1079,6 +1076,13 @@ impl Context {
|
||||
.await?
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
res.insert(
|
||||
"std_header_protection_composing",
|
||||
self.sql
|
||||
.get_raw_config("std_header_protection_composing")
|
||||
.await?
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
|
||||
let elapsed = time_elapsed(&self.creation_time);
|
||||
res.insert("uptime", duration_to_str(elapsed));
|
||||
@@ -1086,147 +1090,6 @@ impl Context {
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
async fn get_self_report(&self) -> Result<String> {
|
||||
#[derive(Default)]
|
||||
struct ChatNumbers {
|
||||
protected: u32,
|
||||
opportunistic_dc: u32,
|
||||
opportunistic_mua: u32,
|
||||
unencrypted_dc: u32,
|
||||
unencrypted_mua: u32,
|
||||
}
|
||||
|
||||
let mut res = String::new();
|
||||
res += &format!("core_version {}\n", get_version_str());
|
||||
|
||||
let num_msgs: u32 = self
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT COUNT(*) FROM msgs WHERE hidden=0 AND chat_id!=?",
|
||||
(DC_CHAT_ID_TRASH,),
|
||||
)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
res += &format!("num_msgs {num_msgs}\n");
|
||||
|
||||
let num_chats: u32 = self
|
||||
.sql
|
||||
.query_get_value("SELECT COUNT(*) FROM chats WHERE id>9 AND blocked!=1", ())
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
res += &format!("num_chats {num_chats}\n");
|
||||
|
||||
let db_size = tokio::fs::metadata(&self.sql.dbfile).await?.len();
|
||||
res += &format!("db_size_bytes {db_size}\n");
|
||||
|
||||
let secret_key = &load_self_secret_key(self).await?.primary_key;
|
||||
let key_created = secret_key.public_key().created_at().timestamp();
|
||||
res += &format!("key_created {key_created}\n");
|
||||
|
||||
// how many of the chats active in the last months are:
|
||||
// - protected
|
||||
// - opportunistic-encrypted and the contact uses Delta Chat
|
||||
// - opportunistic-encrypted and the contact uses a classical MUA
|
||||
// - unencrypted and the contact uses Delta Chat
|
||||
// - unencrypted and the contact uses a classical MUA
|
||||
let three_months_ago = time().saturating_sub(3600 * 24 * 30 * 3);
|
||||
let chats = self
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT c.protected, m.param, m.msgrmsg
|
||||
FROM chats c
|
||||
JOIN msgs m
|
||||
ON c.id=m.chat_id
|
||||
AND m.id=(
|
||||
SELECT id
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND hidden=0
|
||||
AND download_state=?
|
||||
AND to_id!=?
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1)
|
||||
WHERE c.id>9
|
||||
AND (c.blocked=0 OR c.blocked=2)
|
||||
AND IFNULL(m.timestamp,c.created_timestamp) > ?
|
||||
GROUP BY c.id",
|
||||
(DownloadState::Done, ContactId::INFO, three_months_ago),
|
||||
|row| {
|
||||
let protected: ProtectionStatus = row.get(0)?;
|
||||
let message_param: Params =
|
||||
row.get::<_, String>(1)?.parse().unwrap_or_default();
|
||||
let is_dc_message: bool = row.get(2)?;
|
||||
Ok((protected, message_param, is_dc_message))
|
||||
},
|
||||
|rows| {
|
||||
let mut chats = ChatNumbers::default();
|
||||
for row in rows {
|
||||
let (protected, message_param, is_dc_message) = row?;
|
||||
let encrypted = message_param
|
||||
.get_bool(Param::GuaranteeE2ee)
|
||||
.unwrap_or(false);
|
||||
|
||||
if protected == ProtectionStatus::Protected {
|
||||
chats.protected += 1;
|
||||
} else if encrypted {
|
||||
if is_dc_message {
|
||||
chats.opportunistic_dc += 1;
|
||||
} else {
|
||||
chats.opportunistic_mua += 1;
|
||||
}
|
||||
} else if is_dc_message {
|
||||
chats.unencrypted_dc += 1;
|
||||
} else {
|
||||
chats.unencrypted_mua += 1;
|
||||
}
|
||||
}
|
||||
Ok(chats)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
res += &format!("chats_protected {}\n", chats.protected);
|
||||
res += &format!("chats_opportunistic_dc {}\n", chats.opportunistic_dc);
|
||||
res += &format!("chats_opportunistic_mua {}\n", chats.opportunistic_mua);
|
||||
res += &format!("chats_unencrypted_dc {}\n", chats.unencrypted_dc);
|
||||
res += &format!("chats_unencrypted_mua {}\n", chats.unencrypted_mua);
|
||||
|
||||
let self_reporting_id = match self.get_config(Config::SelfReportingId).await? {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
let id = create_id();
|
||||
self.set_config(Config::SelfReportingId, Some(&id)).await?;
|
||||
id
|
||||
}
|
||||
};
|
||||
res += &format!("self_reporting_id {self_reporting_id}");
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Drafts a message with statistics about the usage of Delta Chat.
|
||||
/// The user can inspect the message if they want, and then hit "Send".
|
||||
///
|
||||
/// On the other end, a bot will receive the message and make it available
|
||||
/// to Delta Chat's developers.
|
||||
pub async fn draft_self_report(&self) -> Result<ChatId> {
|
||||
const SELF_REPORTING_BOT_VCARD: &str = include_str!("../assets/self-reporting-bot.vcf");
|
||||
let contact_id: ContactId = *import_vcard(self, SELF_REPORTING_BOT_VCARD)
|
||||
.await?
|
||||
.first()
|
||||
.context("Self reporting bot vCard does not contain a contact")?;
|
||||
mark_contact_id_as_verified(self, contact_id, Some(ContactId::SELF)).await?;
|
||||
|
||||
let chat_id = ChatId::create_for_contact(self, contact_id).await?;
|
||||
chat_id
|
||||
.set_protection(self, ProtectionStatus::Protected, time(), Some(contact_id))
|
||||
.await?;
|
||||
|
||||
let mut msg = Message::new_text(self.get_self_report().await?);
|
||||
|
||||
chat_id.set_draft(self, Some(&mut msg)).await?;
|
||||
|
||||
Ok(chat_id)
|
||||
}
|
||||
|
||||
/// Get a list of fresh, unmuted messages in unblocked chats.
|
||||
///
|
||||
/// The list starts with the most recent message
|
||||
@@ -1236,7 +1099,7 @@ impl Context {
|
||||
pub async fn get_fresh_msgs(&self) -> Result<Vec<MsgId>> {
|
||||
let list = self
|
||||
.sql
|
||||
.query_map(
|
||||
.query_map_vec(
|
||||
concat!(
|
||||
"SELECT m.id",
|
||||
" FROM msgs m",
|
||||
@@ -1253,13 +1116,9 @@ impl Context {
|
||||
" ORDER BY m.timestamp DESC,m.id DESC;"
|
||||
),
|
||||
(MessageState::InFresh, time()),
|
||||
|row| row.get::<_, MsgId>(0),
|
||||
|rows| {
|
||||
let mut list = Vec::new();
|
||||
for row in rows {
|
||||
list.push(row?);
|
||||
}
|
||||
Ok(list)
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
Ok(msg_id)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -1293,7 +1152,7 @@ impl Context {
|
||||
|
||||
let list = self
|
||||
.sql
|
||||
.query_map(
|
||||
.query_map_vec(
|
||||
"SELECT m.id
|
||||
FROM msgs m
|
||||
LEFT JOIN contacts ct
|
||||
@@ -1313,13 +1172,6 @@ impl Context {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
Ok(msg_id)
|
||||
},
|
||||
|rows| {
|
||||
let mut list = Vec::new();
|
||||
for row in rows {
|
||||
list.push(row?);
|
||||
}
|
||||
Ok(list)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(list)
|
||||
@@ -1360,7 +1212,7 @@ impl Context {
|
||||
|
||||
let list = if let Some(chat_id) = chat_id {
|
||||
self.sql
|
||||
.query_map(
|
||||
.query_map_vec(
|
||||
"SELECT m.id AS id
|
||||
FROM msgs m
|
||||
LEFT JOIN contacts ct
|
||||
@@ -1371,13 +1223,9 @@ impl Context {
|
||||
AND IFNULL(txt_normalized, txt) LIKE ?
|
||||
ORDER BY m.timestamp,m.id;",
|
||||
(chat_id, str_like_in_text),
|
||||
|row| row.get::<_, MsgId>("id"),
|
||||
|rows| {
|
||||
let mut ret = Vec::new();
|
||||
for id in rows {
|
||||
ret.push(id?);
|
||||
}
|
||||
Ok(ret)
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get("id")?;
|
||||
Ok(msg_id)
|
||||
},
|
||||
)
|
||||
.await?
|
||||
@@ -1393,7 +1241,7 @@ impl Context {
|
||||
// According to some tests, this limit speeds up eg. 2 character searches by factor 10.
|
||||
// The limit is documented and UI may add a hint when getting 1000 results.
|
||||
self.sql
|
||||
.query_map(
|
||||
.query_map_vec(
|
||||
"SELECT m.id AS id
|
||||
FROM msgs m
|
||||
LEFT JOIN contacts ct
|
||||
@@ -1407,13 +1255,9 @@ impl Context {
|
||||
AND IFNULL(txt_normalized, txt) LIKE ?
|
||||
ORDER BY m.id DESC LIMIT 1000",
|
||||
(str_like_in_text,),
|
||||
|row| row.get::<_, MsgId>("id"),
|
||||
|rows| {
|
||||
let mut ret = Vec::new();
|
||||
for id in rows {
|
||||
ret.push(id?);
|
||||
}
|
||||
Ok(ret)
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get("id")?;
|
||||
Ok(msg_id)
|
||||
},
|
||||
)
|
||||
.await?
|
||||
@@ -1428,12 +1272,6 @@ impl Context {
|
||||
Ok(inbox.as_deref() == Some(folder_name))
|
||||
}
|
||||
|
||||
/// Returns true if given folder name is the name of the "sent" folder.
|
||||
pub async fn is_sentbox(&self, folder_name: &str) -> Result<bool> {
|
||||
let sentbox = self.get_config(Config::ConfiguredSentboxFolder).await?;
|
||||
Ok(sentbox.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?;
|
||||
|
||||
@@ -6,9 +6,9 @@ use super::*;
|
||||
use crate::chat::{Chat, MuteDuration, get_chat_contacts, get_chat_msgs, send_msg, set_muted};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::message::Message;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{E2EE_INFO_MSGS, TestContext, get_chat_msg};
|
||||
use crate::test_utils::{E2EE_INFO_MSGS, TestContext};
|
||||
use crate::tools::{SystemTime, create_outgoing_rfc724_mid};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -276,7 +276,6 @@ async fn test_get_info_completeness() {
|
||||
"mail_port",
|
||||
"mail_security",
|
||||
"notify_about_wrong_pw",
|
||||
"self_reporting_id",
|
||||
"selfstatus",
|
||||
"send_server",
|
||||
"send_user",
|
||||
@@ -296,6 +295,8 @@ async fn test_get_info_completeness() {
|
||||
"webxdc_integration",
|
||||
"device_token",
|
||||
"encrypted_device_token",
|
||||
"stats_last_update",
|
||||
"stats_last_old_contact_id",
|
||||
];
|
||||
let t = TestContext::new().await;
|
||||
let info = t.get_info().await.unwrap();
|
||||
@@ -598,26 +599,6 @@ async fn test_get_next_msgs() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_draft_self_report() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let chat_id = alice.draft_self_report().await?;
|
||||
let msg = get_chat_msg(&alice, chat_id, 0, 1).await;
|
||||
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
|
||||
|
||||
let chat = Chat::load_from_db(&alice, chat_id).await?;
|
||||
assert!(chat.is_protected());
|
||||
|
||||
let mut draft = chat_id.get_draft(&alice).await?.unwrap();
|
||||
assert!(draft.text.starts_with("core_version"));
|
||||
|
||||
// Test that sending into the protected chat works:
|
||||
let _sent = alice.send_msg(chat_id, &mut draft).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_cache_is_cleared_when_io_is_started() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
@@ -3,7 +3,6 @@ use crate::chat::ChatId;
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::{error, info};
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::param::Param;
|
||||
use crate::tools::time;
|
||||
@@ -116,15 +115,13 @@ pub async fn maybe_set_logging_xdc_inner(
|
||||
filename: Option<&str>,
|
||||
msg_id: MsgId,
|
||||
) -> anyhow::Result<()> {
|
||||
if viewtype == Viewtype::Webxdc {
|
||||
if let Some(filename) = filename {
|
||||
if filename.starts_with("debug_logging")
|
||||
&& filename.ends_with(".xdc")
|
||||
&& chat_id.is_self_talk(context).await?
|
||||
{
|
||||
set_debug_logging_xdc(context, Some(msg_id)).await?;
|
||||
}
|
||||
}
|
||||
if viewtype == Viewtype::Webxdc
|
||||
&& let Some(filename) = filename
|
||||
&& filename.starts_with("debug_logging")
|
||||
&& filename.ends_with(".xdc")
|
||||
&& chat_id.is_self_talk(context).await?
|
||||
{
|
||||
set_debug_logging_xdc(context, Some(msg_id)).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -10,17 +10,19 @@ use crate::pgp;
|
||||
|
||||
/// Tries to decrypt a message, but only if it is structured as an Autocrypt message.
|
||||
///
|
||||
/// If successful and the message is encrypted, returns decrypted body.
|
||||
/// If successful and the message was encrypted,
|
||||
/// returns the decrypted and decompressed message.
|
||||
pub fn try_decrypt<'a>(
|
||||
mail: &'a ParsedMail<'a>,
|
||||
private_keyring: &'a [SignedSecretKey],
|
||||
shared_secrets: &[String],
|
||||
) -> Result<Option<::pgp::composed::Message<'static>>> {
|
||||
let Some(encrypted_data_part) = get_encrypted_mime(mail) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let data = encrypted_data_part.get_body_raw()?;
|
||||
let msg = pgp::pk_decrypt(data, private_keyring)?;
|
||||
let msg = pgp::decrypt(data, private_keyring, shared_secrets)?;
|
||||
|
||||
Ok(Some(msg))
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use quick_xml::{
|
||||
|
||||
use crate::simplify::{SimplifiedText, simplify_quote};
|
||||
|
||||
#[derive(Default)]
|
||||
struct Dehtml {
|
||||
strbuilder: String,
|
||||
quote: String,
|
||||
@@ -25,6 +26,9 @@ struct Dehtml {
|
||||
/// Everything between `<div name="quote">` and `<div name="quoted-content">` is usually metadata
|
||||
/// If this is > `0`, then we are inside a `<div name="quoted-content">`.
|
||||
divs_since_quoted_content_div: u32,
|
||||
/// `<div class="header-protection-legacy-display">` elements should be omitted, see
|
||||
/// <https://www.rfc-editor.org/rfc/rfc9788.html#section-4.5.3.3>.
|
||||
divs_since_hp_legacy_display: u32,
|
||||
/// All-Inkl just puts the quote into `<blockquote> </blockquote>`. This count is
|
||||
/// increased at each `<blockquote>` and decreased at each `</blockquote>`.
|
||||
blockquotes_since_blockquote: u32,
|
||||
@@ -48,20 +52,25 @@ impl Dehtml {
|
||||
}
|
||||
|
||||
fn get_add_text(&self) -> AddText {
|
||||
if self.divs_since_quote_div > 0 && self.divs_since_quoted_content_div == 0 {
|
||||
AddText::No // Everything between `<div name="quoted">` and `<div name="quoted_content">` is metadata which we don't want
|
||||
// Everything between `<div name="quoted">` and `<div name="quoted_content">` is
|
||||
// metadata which we don't want.
|
||||
if self.divs_since_quote_div > 0 && self.divs_since_quoted_content_div == 0
|
||||
|| self.divs_since_hp_legacy_display > 0
|
||||
{
|
||||
AddText::No
|
||||
} else {
|
||||
self.add_text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
#[derive(Debug, Default, PartialEq, Clone, Copy)]
|
||||
enum AddText {
|
||||
/// Inside `<script>`, `<style>` and similar tags
|
||||
/// which contents should not be displayed.
|
||||
No,
|
||||
|
||||
#[default]
|
||||
YesRemoveLineEnds,
|
||||
|
||||
/// Inside `<pre>`.
|
||||
@@ -121,12 +130,7 @@ fn dehtml_quick_xml(buf: &str) -> (String, String) {
|
||||
|
||||
let mut dehtml = Dehtml {
|
||||
strbuilder: String::with_capacity(buf.len()),
|
||||
quote: String::new(),
|
||||
add_text: AddText::YesRemoveLineEnds,
|
||||
last_href: None,
|
||||
divs_since_quote_div: 0,
|
||||
divs_since_quoted_content_div: 0,
|
||||
blockquotes_since_blockquote: 0,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut reader = quick_xml::Reader::from_str(buf);
|
||||
@@ -244,6 +248,7 @@ fn dehtml_endtag_cb(event: &BytesEnd, dehtml: &mut Dehtml) {
|
||||
"div" => {
|
||||
pop_tag(&mut dehtml.divs_since_quote_div);
|
||||
pop_tag(&mut dehtml.divs_since_quoted_content_div);
|
||||
pop_tag(&mut dehtml.divs_since_hp_legacy_display);
|
||||
|
||||
*dehtml.get_buf() += "\n\n";
|
||||
dehtml.add_text = AddText::YesRemoveLineEnds;
|
||||
@@ -295,6 +300,8 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
|
||||
"div" => {
|
||||
maybe_push_tag(event, reader, "quote", &mut dehtml.divs_since_quote_div);
|
||||
maybe_push_tag(event, reader, "quoted-content", &mut dehtml.divs_since_quoted_content_div);
|
||||
maybe_push_tag(event, reader, "header-protection-legacy-display",
|
||||
&mut dehtml.divs_since_hp_legacy_display);
|
||||
|
||||
*dehtml.get_buf() += "\n\n";
|
||||
dehtml.add_text = AddText::YesRemoveLineEnds;
|
||||
@@ -539,6 +546,27 @@ mod tests {
|
||||
assert_eq!(txt.text.trim(), "two\nlines");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hp_legacy_display() {
|
||||
let input = r#"
|
||||
<html><head><title></title></head><body>
|
||||
<div class="header-protection-legacy-display">
|
||||
<pre>Subject: Dinner plans</pre>
|
||||
</div>
|
||||
<p>
|
||||
Let's meet at Rama's Roti Shop at 8pm and go to the park
|
||||
from there.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
"#;
|
||||
let txt = dehtml(input).unwrap();
|
||||
assert_eq!(
|
||||
txt.text.trim(),
|
||||
"Let's meet at Rama's Roti Shop at 8pm and go to the park from there."
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_quote_div() {
|
||||
let input = include_str!("../test-data/message/gmx-quote-body.eml");
|
||||
|
||||
@@ -10,7 +10,6 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::imap::session::Session;
|
||||
use crate::log::info;
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, Part};
|
||||
use crate::tools::time;
|
||||
@@ -157,30 +156,23 @@ pub(crate) async fn download_msg(
|
||||
let row = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT uid, folder, uidvalidity FROM imap WHERE rfc724_mid=? AND target!=''",
|
||||
"SELECT uid, folder FROM imap WHERE rfc724_mid=? AND target!=''",
|
||||
(&msg.rfc724_mid,),
|
||||
|row| {
|
||||
let server_uid: u32 = row.get(0)?;
|
||||
let server_folder: String = row.get(1)?;
|
||||
let uidvalidity: u32 = row.get(2)?;
|
||||
Ok((server_uid, server_folder, uidvalidity))
|
||||
Ok((server_uid, server_folder))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let Some((server_uid, server_folder, uidvalidity)) = row else {
|
||||
let Some((server_uid, server_folder)) = row else {
|
||||
// No IMAP record found, we don't know the UID and folder.
|
||||
return Err(anyhow!("Call download_full() again to try over."));
|
||||
};
|
||||
|
||||
session
|
||||
.fetch_single_msg(
|
||||
context,
|
||||
&server_folder,
|
||||
uidvalidity,
|
||||
server_uid,
|
||||
msg.rfc724_mid.clone(),
|
||||
)
|
||||
.fetch_single_msg(context, &server_folder, server_uid, msg.rfc724_mid.clone())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -194,7 +186,6 @@ impl Session {
|
||||
&mut self,
|
||||
context: &Context,
|
||||
folder: &str,
|
||||
uidvalidity: u32,
|
||||
uid: u32,
|
||||
rfc724_mid: String,
|
||||
) -> Result<()> {
|
||||
@@ -214,16 +205,8 @@ 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,
|
||||
uidvalidity,
|
||||
vec![uid],
|
||||
&uid_message_ids,
|
||||
false,
|
||||
sender,
|
||||
)
|
||||
.await?;
|
||||
self.fetch_many_msgs(context, folder, vec![uid], &uid_message_ids, false, sender)
|
||||
.await?;
|
||||
if receiver.recv().await.is_err() {
|
||||
bail!("Failed to fetch UID {uid}");
|
||||
}
|
||||
@@ -237,21 +220,14 @@ impl MimeMessage {
|
||||
/// 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;
|
||||
/// `error` is set as the part error;
|
||||
/// in the future, we may do more advanced things as previews here.
|
||||
/// 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,
|
||||
error: Option<String>,
|
||||
) -> Result<()> {
|
||||
let prefix = match error {
|
||||
None => "",
|
||||
Some(_) => "[❗] ",
|
||||
};
|
||||
let mut text = format!(
|
||||
"{prefix}[{}]",
|
||||
"[{}]",
|
||||
stock_str::partial_download_msg_body(context, org_bytes).await
|
||||
);
|
||||
if let Some(delete_server_after) = context.get_config_delete_server_after().await? {
|
||||
@@ -268,7 +244,6 @@ impl MimeMessage {
|
||||
self.do_add_single_part(Part {
|
||||
typ: Viewtype::Text,
|
||||
msg: text,
|
||||
error,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
|
||||
31
src/e2ee.rs
31
src/e2ee.rs
@@ -46,6 +46,7 @@ impl EncryptHelper {
|
||||
keyring: Vec<SignedPublicKey>,
|
||||
mail_to_encrypt: MimePart<'static>,
|
||||
compress: bool,
|
||||
anonymous_recipients: bool,
|
||||
) -> Result<String> {
|
||||
let sign_key = load_self_secret_key(context).await?;
|
||||
|
||||
@@ -53,7 +54,35 @@ impl EncryptHelper {
|
||||
let cursor = Cursor::new(&mut raw_message);
|
||||
mail_to_encrypt.clone().write_part(cursor).ok();
|
||||
|
||||
let ctext = pgp::pk_encrypt(raw_message, keyring, Some(sign_key), compress).await?;
|
||||
let ctext = pgp::pk_encrypt(
|
||||
raw_message,
|
||||
keyring,
|
||||
Some(sign_key),
|
||||
compress,
|
||||
anonymous_recipients,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(ctext)
|
||||
}
|
||||
|
||||
/// Symmetrically encrypt the message. This is used for broadcast channels.
|
||||
/// `shared secret` is the secret that will be used for symmetric encryption.
|
||||
pub async fn encrypt_symmetrically(
|
||||
self,
|
||||
context: &Context,
|
||||
shared_secret: &str,
|
||||
mail_to_encrypt: MimePart<'static>,
|
||||
compress: bool,
|
||||
) -> Result<String> {
|
||||
let sign_key = load_self_secret_key(context).await?;
|
||||
|
||||
let mut raw_message = Vec::new();
|
||||
let cursor = Cursor::new(&mut raw_message);
|
||||
mail_to_encrypt.clone().write_part(cursor).ok();
|
||||
|
||||
let ctext =
|
||||
pgp::symm_encrypt_message(raw_message, sign_key, shared_secret, compress).await?;
|
||||
|
||||
Ok(ctext)
|
||||
}
|
||||
|
||||
@@ -80,17 +80,18 @@ use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::download::MIN_DELETE_SERVER_AFTER;
|
||||
use crate::events::EventType;
|
||||
use crate::location;
|
||||
use crate::log::{LogExt, error, info, warn};
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::message::{Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::stock_str;
|
||||
use crate::tools::{SystemTime, duration_to_str, time};
|
||||
use crate::{location, stats};
|
||||
|
||||
/// Ephemeral timer value.
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, Default)]
|
||||
pub enum Timer {
|
||||
/// Timer is disabled.
|
||||
#[default]
|
||||
Disabled,
|
||||
|
||||
/// Timer is enabled.
|
||||
@@ -125,12 +126,6 @@ impl Timer {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Timer {
|
||||
fn default() -> Self {
|
||||
Self::Disabled
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Timer {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.to_u32())
|
||||
@@ -246,10 +241,9 @@ pub(crate) async fn stock_ephemeral_timer_changed(
|
||||
match timer {
|
||||
Timer::Disabled => stock_str::msg_ephemeral_timer_disabled(context, from_id).await,
|
||||
Timer::Enabled { duration } => match duration {
|
||||
0..=59 => {
|
||||
0..=60 => {
|
||||
stock_str::msg_ephemeral_timer_enabled(context, &timer.to_string(), from_id).await
|
||||
}
|
||||
60 => stock_str::msg_ephemeral_timer_minute(context, from_id).await,
|
||||
61..=3599 => {
|
||||
stock_str::msg_ephemeral_timer_minutes(
|
||||
context,
|
||||
@@ -386,7 +380,7 @@ async fn select_expired_messages(
|
||||
) -> Result<Vec<(MsgId, ChatId, Viewtype, u32)>> {
|
||||
let mut rows = context
|
||||
.sql
|
||||
.query_map(
|
||||
.query_map_vec(
|
||||
r#"
|
||||
SELECT id, chat_id, type, location_id
|
||||
FROM msgs
|
||||
@@ -407,7 +401,6 @@ WHERE
|
||||
let location_id: u32 = row.get("location_id")?;
|
||||
Ok((id, chat_id, viewtype, location_id))
|
||||
},
|
||||
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -425,7 +418,7 @@ WHERE
|
||||
|
||||
let rows_expired = context
|
||||
.sql
|
||||
.query_map(
|
||||
.query_map_vec(
|
||||
r#"
|
||||
SELECT id, chat_id, type, location_id
|
||||
FROM msgs
|
||||
@@ -453,7 +446,6 @@ WHERE
|
||||
let location_id: u32 = row.get("location_id")?;
|
||||
Ok((id, chat_id, viewtype, location_id))
|
||||
},
|
||||
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -610,7 +602,7 @@ pub(crate) async fn ephemeral_loop(context: &Context, interrupt_receiver: Receiv
|
||||
+ Duration::from_secs(1)
|
||||
} else {
|
||||
// no messages to be deleted for now, wait long for one to occur
|
||||
now + Duration::from_secs(86400)
|
||||
now + Duration::from_secs(86400) // 1 day
|
||||
};
|
||||
|
||||
if let Ok(duration) = until.duration_since(now) {
|
||||
@@ -637,6 +629,12 @@ pub(crate) async fn ephemeral_loop(context: &Context, interrupt_receiver: Receiv
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure that the statistics stay correct by updating them _before_ deleting messages:
|
||||
stats::maybe_update_message_stats(context)
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
|
||||
delete_expired_messages(context, time())
|
||||
.await
|
||||
.log_err(context)
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use crate::timesmearing::MAX_SECONDS_TO_LEND_FROM_FUTURE;
|
||||
use crate::{
|
||||
chat::{self, Chat, ChatItem, ProtectionStatus, create_group_chat, send_text_msg},
|
||||
chat::{self, Chat, ChatItem, create_group, send_text_msg},
|
||||
tools::IsNoneOrEmpty,
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ async fn test_stock_ephemeral_messages() {
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 60 }, ContactId::SELF)
|
||||
.await,
|
||||
"You set message deletion timer to 1 minute."
|
||||
"You set message deletion timer to 60 s."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 90 }, ContactId::SELF)
|
||||
@@ -142,7 +142,7 @@ async fn test_ephemeral_enable_disable() -> Result<()> {
|
||||
let bob_received_message = bob.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
bob_received_message.text,
|
||||
"Message deletion timer is set to 1 minute by alice@example.org."
|
||||
"Message deletion timer is set to 60 s by alice@example.org."
|
||||
);
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(bob).await?,
|
||||
@@ -164,7 +164,7 @@ async fn test_ephemeral_enable_disable() -> Result<()> {
|
||||
async fn test_ephemeral_unpromoted() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group name").await?;
|
||||
let chat_id = create_group(&alice, "Group name").await?;
|
||||
|
||||
// Group is unpromoted, the timer can be changed without sending a message.
|
||||
assert!(chat_id.is_unpromoted(&alice).await?);
|
||||
@@ -799,8 +799,7 @@ async fn test_ephemeral_timer_non_member() -> Result<()> {
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let alice_chat_id =
|
||||
create_group_chat(alice, ProtectionStatus::Unprotected, "Group name").await?;
|
||||
let alice_chat_id = create_group(alice, "Group name").await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
send_text_msg(alice, alice_chat_id, "Hi!".to_string()).await?;
|
||||
|
||||
|
||||
@@ -66,8 +66,7 @@ mod test_chatlist_events {
|
||||
use crate::{
|
||||
EventType,
|
||||
chat::{
|
||||
self, ChatId, ChatVisibility, MuteDuration, ProtectionStatus, create_broadcast,
|
||||
create_group_chat, set_muted,
|
||||
self, ChatId, ChatVisibility, MuteDuration, create_broadcast, create_group, set_muted,
|
||||
},
|
||||
config::Config,
|
||||
constants::*,
|
||||
@@ -138,12 +137,7 @@ mod test_chatlist_events {
|
||||
async fn test_change_chat_visibility() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let chat_id = create_group_chat(
|
||||
&alice,
|
||||
crate::chat::ProtectionStatus::Unprotected,
|
||||
"my_group",
|
||||
)
|
||||
.await?;
|
||||
let chat_id = create_group(&alice, "my_group").await?;
|
||||
|
||||
chat_id
|
||||
.set_visibility(&alice, ChatVisibility::Pinned)
|
||||
@@ -289,7 +283,7 @@ mod test_chatlist_events {
|
||||
async fn test_delete_chat() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
|
||||
let chat = create_group(&alice, "My Group").await?;
|
||||
|
||||
alice.evtracker.clear_events();
|
||||
chat.delete(&alice).await?;
|
||||
@@ -299,11 +293,11 @@ mod test_chatlist_events {
|
||||
|
||||
/// Create group chat
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_group_chat() -> Result<()> {
|
||||
async fn test_create_group() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
alice.evtracker.clear_events();
|
||||
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
|
||||
let chat = create_group(&alice, "My Group").await?;
|
||||
wait_for_chatlist_and_specific_item(&alice, chat).await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -324,7 +318,7 @@ mod test_chatlist_events {
|
||||
async fn test_mute_chat() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
|
||||
let chat = create_group(&alice, "My Group").await?;
|
||||
|
||||
alice.evtracker.clear_events();
|
||||
chat::set_muted(&alice, chat, MuteDuration::Forever).await?;
|
||||
@@ -343,7 +337,7 @@ mod test_chatlist_events {
|
||||
async fn test_mute_chat_expired() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
|
||||
let chat = create_group(&alice, "My Group").await?;
|
||||
|
||||
let mute_duration = MuteDuration::Until(
|
||||
std::time::SystemTime::now()
|
||||
@@ -363,7 +357,7 @@ mod test_chatlist_events {
|
||||
async fn test_change_chat_name() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
|
||||
let chat = create_group(&alice, "My Group").await?;
|
||||
|
||||
alice.evtracker.clear_events();
|
||||
chat::set_chat_name(&alice, chat, "New Name").await?;
|
||||
@@ -377,7 +371,7 @@ mod test_chatlist_events {
|
||||
async fn test_change_chat_profile_image() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
|
||||
let chat = create_group(&alice, "My Group").await?;
|
||||
|
||||
alice.evtracker.clear_events();
|
||||
let file = alice.dir.path().join("avatar.png");
|
||||
@@ -395,9 +389,7 @@ mod test_chatlist_events {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let chat = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[&bob])
|
||||
.await;
|
||||
let chat = alice.create_group_with_members("My Group", &[&bob]).await;
|
||||
|
||||
let sent_msg = alice.send_text(chat, "Hello").await;
|
||||
let chat_id_for_bob = bob.recv_msg(&sent_msg).await.chat_id;
|
||||
@@ -419,9 +411,7 @@ mod test_chatlist_events {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let chat = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[&bob])
|
||||
.await;
|
||||
let chat = alice.create_group_with_members("My Group", &[&bob]).await;
|
||||
let sent_msg = alice.send_text(chat, "Hello").await;
|
||||
let chat_id_for_bob = bob.recv_msg(&sent_msg).await.chat_id;
|
||||
|
||||
@@ -438,9 +428,7 @@ mod test_chatlist_events {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let chat = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[&bob])
|
||||
.await;
|
||||
let chat = alice.create_group_with_members("My Group", &[&bob]).await;
|
||||
let sent_msg = alice.send_text(chat, "Hello").await;
|
||||
let chat_id_for_bob = bob.recv_msg(&sent_msg).await.chat_id;
|
||||
|
||||
@@ -456,7 +444,7 @@ mod test_chatlist_events {
|
||||
async fn test_delete_message() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
|
||||
let chat = create_group(&alice, "My Group").await?;
|
||||
let message = chat::send_text_msg(&alice, chat, "Hello World".to_owned()).await?;
|
||||
|
||||
alice.evtracker.clear_events();
|
||||
@@ -473,9 +461,7 @@ mod test_chatlist_events {
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
let chat = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[&bob])
|
||||
.await;
|
||||
let chat = alice.create_group_with_members("My Group", &[&bob]).await;
|
||||
let sent_msg = alice.send_text(chat, "Hello").await;
|
||||
let chat_id_for_bob = bob.recv_msg(&sent_msg).await.chat_id;
|
||||
chat_id_for_bob.accept(&bob).await?;
|
||||
@@ -516,7 +502,7 @@ mod test_chatlist_events {
|
||||
async fn test_update_after_ephemeral_messages() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
|
||||
let chat = create_group(&alice, "My Group").await?;
|
||||
chat.set_ephemeral_timer(&alice, crate::ephemeral::Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
alice
|
||||
@@ -560,8 +546,7 @@ First thread."#;
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
let alice_chatid =
|
||||
chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat").await?;
|
||||
let alice_chatid = chat::create_group(&alice.ctx, "the chat").await?;
|
||||
|
||||
// Step 1: Generate QR-code, secure-join implied by chatid
|
||||
let qr = get_securejoin_qr(&alice.ctx, Some(alice_chatid)).await?;
|
||||
@@ -608,7 +593,7 @@ First thread."#;
|
||||
async fn test_resend_message() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
|
||||
let chat = create_group(&alice, "My Group").await?;
|
||||
|
||||
let msg_id = chat::send_text_msg(&alice, chat, "Hello".to_owned()).await?;
|
||||
let _ = alice.pop_sent_msg().await;
|
||||
@@ -628,7 +613,7 @@ First thread."#;
|
||||
async fn test_reaction() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
|
||||
let chat = create_group(&alice, "My Group").await?;
|
||||
let msg_id = chat::send_text_msg(&alice, chat, "Hello".to_owned()).await?;
|
||||
let _ = alice.pop_sent_msg().await;
|
||||
|
||||
|
||||
@@ -39,6 +39,8 @@ pub enum HeaderDef {
|
||||
/// Mailing list ID defined in [RFC 2919](https://tools.ietf.org/html/rfc2919).
|
||||
ListId,
|
||||
ListPost,
|
||||
/// Mailing list id, belonging to a broadcast channel created by Delta Chat
|
||||
ChatListId,
|
||||
|
||||
/// List-Help header defined in [RFC 2369](https://datatracker.ietf.org/doc/html/rfc2369).
|
||||
ListHelp,
|
||||
@@ -63,7 +65,9 @@ pub enum HeaderDef {
|
||||
ChatUserAvatar,
|
||||
ChatVoiceMessage,
|
||||
ChatGroupMemberRemoved,
|
||||
ChatGroupMemberRemovedFpr,
|
||||
ChatGroupMemberAdded,
|
||||
ChatGroupMemberAddedFpr,
|
||||
ChatContent,
|
||||
|
||||
/// Past members of the group.
|
||||
@@ -94,6 +98,11 @@ pub enum HeaderDef {
|
||||
/// This message obsoletes the text of the message defined here by rfc724_mid.
|
||||
ChatEdit,
|
||||
|
||||
/// The secret shared amongst all recipients of this broadcast channel,
|
||||
/// used to encrypt and decrypt messages.
|
||||
/// This secret is sent to a new member in the member-addition message.
|
||||
ChatBroadcastSecret,
|
||||
|
||||
/// [Autocrypt](https://autocrypt.org/) header.
|
||||
Autocrypt,
|
||||
AutocryptGossip,
|
||||
@@ -129,6 +138,9 @@ pub enum HeaderDef {
|
||||
/// Advertised gossip topic for one webxdc.
|
||||
IrohGossipTopic,
|
||||
|
||||
/// See <https://www.rfc-editor.org/rfc/rfc9788.html#name-hp-outer-header-field>.
|
||||
HpOuter,
|
||||
|
||||
#[cfg(test)]
|
||||
TestHeader,
|
||||
}
|
||||
|
||||
87
src/html.rs
87
src/html.rs
@@ -169,27 +169,28 @@ impl HtmlMsgParser {
|
||||
MimeMultipartType::Single => {
|
||||
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
|
||||
if mimetype == mime::TEXT_HTML {
|
||||
if self.html.is_empty() {
|
||||
if let Ok(decoded_data) = mail.get_body() {
|
||||
self.html = decoded_data;
|
||||
}
|
||||
}
|
||||
} else if mimetype == mime::TEXT_PLAIN && self.plain.is_none() {
|
||||
if let Ok(decoded_data) = mail.get_body() {
|
||||
self.plain = Some(PlainText {
|
||||
text: decoded_data,
|
||||
flowed: if let Some(format) = mail.ctype.params.get("format") {
|
||||
format.as_str().eq_ignore_ascii_case("flowed")
|
||||
} else {
|
||||
false
|
||||
},
|
||||
delsp: if let Some(delsp) = mail.ctype.params.get("delsp") {
|
||||
delsp.as_str().eq_ignore_ascii_case("yes")
|
||||
} else {
|
||||
false
|
||||
},
|
||||
});
|
||||
if self.html.is_empty()
|
||||
&& let Ok(decoded_data) = mail.get_body()
|
||||
{
|
||||
self.html = decoded_data;
|
||||
}
|
||||
} else if mimetype == mime::TEXT_PLAIN
|
||||
&& self.plain.is_none()
|
||||
&& let Ok(decoded_data) = mail.get_body()
|
||||
{
|
||||
self.plain = Some(PlainText {
|
||||
text: decoded_data,
|
||||
flowed: if let Some(format) = mail.ctype.params.get("format") {
|
||||
format.as_str().eq_ignore_ascii_case("flowed")
|
||||
} else {
|
||||
false
|
||||
},
|
||||
delsp: if let Some(delsp) = mail.ctype.params.get("delsp") {
|
||||
delsp.as_str().eq_ignore_ascii_case("yes")
|
||||
} else {
|
||||
false
|
||||
},
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -213,31 +214,29 @@ impl HtmlMsgParser {
|
||||
MimeMultipartType::Message => Ok(()),
|
||||
MimeMultipartType::Single => {
|
||||
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
|
||||
if mimetype.type_() == mime::IMAGE {
|
||||
if let Some(cid) = mail.headers.get_header_value(HeaderDef::ContentId) {
|
||||
if let Ok(cid) = parse_message_id(&cid) {
|
||||
if let Ok(replacement) = mimepart_to_data_url(mail) {
|
||||
let re_string = format!(
|
||||
"(<img[^>]*src[^>]*=[^>]*)(cid:{})([^>]*>)",
|
||||
regex::escape(&cid)
|
||||
);
|
||||
match regex::Regex::new(&re_string) {
|
||||
Ok(re) => {
|
||||
self.html = re
|
||||
.replace_all(
|
||||
&self.html,
|
||||
format!("${{1}}{replacement}${{3}}").as_str(),
|
||||
)
|
||||
.as_ref()
|
||||
.to_string()
|
||||
}
|
||||
Err(e) => warn!(
|
||||
context,
|
||||
"Cannot create regex for cid: {} throws {}", re_string, e
|
||||
),
|
||||
}
|
||||
}
|
||||
if mimetype.type_() == mime::IMAGE
|
||||
&& let Some(cid) = mail.headers.get_header_value(HeaderDef::ContentId)
|
||||
&& let Ok(cid) = parse_message_id(&cid)
|
||||
&& let Ok(replacement) = mimepart_to_data_url(mail)
|
||||
{
|
||||
let re_string = format!(
|
||||
"(<img[^>]*src[^>]*=[^>]*)(cid:{})([^>]*>)",
|
||||
regex::escape(&cid)
|
||||
);
|
||||
match regex::Regex::new(&re_string) {
|
||||
Ok(re) => {
|
||||
self.html = re
|
||||
.replace_all(
|
||||
&self.html,
|
||||
format!("${{1}}{replacement}${{3}}").as_str(),
|
||||
)
|
||||
.as_ref()
|
||||
.to_string()
|
||||
}
|
||||
Err(e) => warn!(
|
||||
context,
|
||||
"Cannot create regex for cid: {} throws {}", re_string, e
|
||||
),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
313
src/imap.rs
313
src/imap.rs
@@ -20,12 +20,11 @@ use deltachat_contact_tools::ContactAddress;
|
||||
use futures::{FutureExt as _, TryStreamExt};
|
||||
use futures_lite::FutureExt;
|
||||
use num_traits::FromPrimitive;
|
||||
use rand::Rng;
|
||||
use ratelimit::Ratelimit;
|
||||
use url::Url;
|
||||
|
||||
use crate::calls::{create_fallback_ice_servers, create_ice_servers_from_metadata};
|
||||
use crate::chat::{self, ChatId, ChatIdBlocked};
|
||||
use crate::chat::{self, ChatId, ChatIdBlocked, add_device_msg};
|
||||
use crate::chatlist_events;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{self, Blocked, Chattype, ShowEmails};
|
||||
@@ -33,10 +32,7 @@ use crate::contact::{Contact, ContactId, Modifier, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::log::{LogExt, error, info, warn};
|
||||
use crate::login_param::{
|
||||
ConfiguredLoginParam, ConfiguredServerLoginParam, prioritize_server_login_params,
|
||||
};
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::message::{self, Message, MessageState, MessengerMessage, MsgId};
|
||||
use crate::mimeparser;
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
@@ -49,6 +45,9 @@ use crate::receive_imf::{
|
||||
use crate::scheduler::connectivity::ConnectivityStore;
|
||||
use crate::stock_str;
|
||||
use crate::tools::{self, create_id, duration_to_str, time};
|
||||
use crate::transport::{
|
||||
ConfiguredLoginParam, ConfiguredServerLoginParam, prioritize_server_login_params,
|
||||
};
|
||||
|
||||
pub(crate) mod capabilities;
|
||||
mod client;
|
||||
@@ -105,6 +104,12 @@ pub(crate) struct Imap {
|
||||
/// immediately after logging in or returning an error in response to LOGIN command
|
||||
/// due to internal server error.
|
||||
ratelimit: Ratelimit,
|
||||
|
||||
/// IMAP UID resync request sender.
|
||||
pub(crate) resync_request_sender: async_channel::Sender<()>,
|
||||
|
||||
/// IMAP UID resync request receiver.
|
||||
pub(crate) resync_request_receiver: async_channel::Receiver<()>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -157,7 +162,6 @@ pub enum FolderMeaning {
|
||||
Spam,
|
||||
Inbox,
|
||||
Mvbox,
|
||||
Sent,
|
||||
Trash,
|
||||
|
||||
/// Virtual folders.
|
||||
@@ -176,7 +180,6 @@ impl FolderMeaning {
|
||||
FolderMeaning::Spam => None,
|
||||
FolderMeaning::Inbox => Some(Config::ConfiguredInboxFolder),
|
||||
FolderMeaning::Mvbox => Some(Config::ConfiguredMvboxFolder),
|
||||
FolderMeaning::Sent => Some(Config::ConfiguredSentboxFolder),
|
||||
FolderMeaning::Trash => Some(Config::ConfiguredTrashFolder),
|
||||
FolderMeaning::Virtual => None,
|
||||
}
|
||||
@@ -257,6 +260,7 @@ impl Imap {
|
||||
oauth2: bool,
|
||||
idle_interrupt_receiver: Receiver<()>,
|
||||
) -> Self {
|
||||
let (resync_request_sender, resync_request_receiver) = async_channel::bounded(1);
|
||||
Imap {
|
||||
idle_interrupt_receiver,
|
||||
addr: addr.to_string(),
|
||||
@@ -271,6 +275,8 @@ impl Imap {
|
||||
conn_backoff_ms: 0,
|
||||
// 1 connection per minute + a burst of 2.
|
||||
ratelimit: Ratelimit::new(Duration::new(120, 0), 2.0),
|
||||
resync_request_sender,
|
||||
resync_request_receiver,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,9 +348,9 @@ impl Imap {
|
||||
const BACKOFF_MIN_MS: u64 = 2000;
|
||||
const BACKOFF_MAX_MS: u64 = 80_000;
|
||||
self.conn_backoff_ms = min(self.conn_backoff_ms, BACKOFF_MAX_MS / 2);
|
||||
self.conn_backoff_ms = self.conn_backoff_ms.saturating_add(
|
||||
rand::thread_rng().gen_range((self.conn_backoff_ms / 2)..=self.conn_backoff_ms),
|
||||
);
|
||||
self.conn_backoff_ms = self.conn_backoff_ms.saturating_add(rand::random_range(
|
||||
(self.conn_backoff_ms / 2)..=self.conn_backoff_ms,
|
||||
));
|
||||
self.conn_backoff_ms = max(BACKOFF_MIN_MS, self.conn_backoff_ms);
|
||||
|
||||
let login_params = prioritize_server_login_params(&context.sql, &self.lp, "imap").await?;
|
||||
@@ -395,6 +401,7 @@ impl Imap {
|
||||
match login_res {
|
||||
Ok(mut session) => {
|
||||
let capabilities = determine_capabilities(&mut session).await?;
|
||||
let resync_request_sender = self.resync_request_sender.clone();
|
||||
|
||||
let session = if capabilities.can_compress {
|
||||
info!(context, "Enabling IMAP compression.");
|
||||
@@ -405,9 +412,9 @@ impl Imap {
|
||||
})
|
||||
.await
|
||||
.context("Failed to enable IMAP compression")?;
|
||||
Session::new(compressed_session, capabilities)
|
||||
Session::new(compressed_session, capabilities, resync_request_sender)
|
||||
} else {
|
||||
Session::new(session, capabilities)
|
||||
Session::new(session, capabilities, resync_request_sender)
|
||||
};
|
||||
|
||||
// Store server ID in the context to display in account info.
|
||||
@@ -620,71 +627,38 @@ impl Imap {
|
||||
|
||||
// Determine the target folder where the message should be moved to.
|
||||
//
|
||||
// If we have seen the message on the IMAP server before, do not move it.
|
||||
// We only move the messages from the INBOX and Spam folders.
|
||||
// This is required to avoid infinite MOVE loop on IMAP servers
|
||||
// that alias `DeltaChat` folder to other names.
|
||||
// For example, some Dovecot servers alias `DeltaChat` folder to `INBOX.DeltaChat`.
|
||||
// In this case Delta Chat configured with `DeltaChat` as the destination folder
|
||||
// would detect messages in the `INBOX.DeltaChat` folder
|
||||
// and try to move them to the `DeltaChat` folder.
|
||||
// Such move to the same folder results in the messages
|
||||
// getting a new UID, so the messages will be detected as new
|
||||
// In this case moving from `INBOX.DeltaChat` to `DeltaChat`
|
||||
// results in the messages getting a new UID,
|
||||
// so the messages will be detected as new
|
||||
// in the `INBOX.DeltaChat` folder again.
|
||||
let _target;
|
||||
let target = if let Some(message_id) = &message_id {
|
||||
let msg_info =
|
||||
message::rfc724_mid_exists_ex(context, message_id, "deleted=1").await?;
|
||||
let delete = if let Some((_, _, true)) = msg_info {
|
||||
info!(context, "Deleting locally deleted message {message_id}.");
|
||||
true
|
||||
} else if let Some((_, ts_sent_old, _)) = msg_info {
|
||||
let is_chat_msg = headers.get_header_value(HeaderDef::ChatVersion).is_some();
|
||||
let ts_sent = headers
|
||||
.get_header_value(HeaderDef::Date)
|
||||
.and_then(|v| mailparse::dateparse(&v).ok())
|
||||
.unwrap_or_default();
|
||||
let is_dup = is_dup_msg(is_chat_msg, ts_sent, ts_sent_old);
|
||||
if is_dup {
|
||||
info!(context, "Deleting duplicate message {message_id}.");
|
||||
}
|
||||
is_dup
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if delete {
|
||||
&delete_target
|
||||
} else if context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT (*) FROM imap WHERE rfc724_mid=?",
|
||||
(message_id,),
|
||||
)
|
||||
let delete = if let Some(message_id) = &message_id {
|
||||
message::rfc724_mid_exists_ex(context, message_id, "deleted=1")
|
||||
.await?
|
||||
{
|
||||
info!(
|
||||
context,
|
||||
"Not moving the message {} that we have seen before.", &message_id
|
||||
);
|
||||
folder
|
||||
} else {
|
||||
_target = target_folder(context, folder, folder_meaning, &headers).await?;
|
||||
&_target
|
||||
}
|
||||
.is_some_and(|(_msg_id, deleted)| deleted)
|
||||
} else {
|
||||
// Do not move the messages without Message-ID.
|
||||
// We cannot reliably determine if we have seen them before,
|
||||
// so it is safer not to move them.
|
||||
warn!(
|
||||
context,
|
||||
"Not moving the message that does not have a Message-ID."
|
||||
);
|
||||
folder
|
||||
false
|
||||
};
|
||||
|
||||
// Generate a fake Message-ID to identify the message in the database
|
||||
// if the message has no real Message-ID.
|
||||
let message_id = message_id.unwrap_or_else(create_message_id);
|
||||
|
||||
if delete {
|
||||
info!(context, "Deleting locally deleted message {message_id}.");
|
||||
}
|
||||
|
||||
let _target;
|
||||
let target = if delete {
|
||||
&delete_target
|
||||
} else {
|
||||
_target = target_folder(context, folder, folder_meaning, &headers).await?;
|
||||
&_target
|
||||
};
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
@@ -768,7 +742,6 @@ impl Imap {
|
||||
.fetch_many_msgs(
|
||||
context,
|
||||
folder,
|
||||
uid_validity,
|
||||
uids_fetch_in_batch.split_off(0),
|
||||
&uid_message_ids,
|
||||
fetch_partially,
|
||||
@@ -833,9 +806,6 @@ impl Imap {
|
||||
context: &Context,
|
||||
session: &mut Session,
|
||||
) -> Result<()> {
|
||||
add_all_recipients_as_contacts(context, session, Config::ConfiguredSentboxFolder)
|
||||
.await
|
||||
.context("failed to get recipients from the sentbox")?;
|
||||
add_all_recipients_as_contacts(context, session, Config::ConfiguredMvboxFolder)
|
||||
.await
|
||||
.context("failed to get recipients from the movebox")?;
|
||||
@@ -1070,7 +1040,7 @@ impl Session {
|
||||
async fn move_delete_messages(&mut self, context: &Context, folder: &str) -> Result<()> {
|
||||
let rows = context
|
||||
.sql
|
||||
.query_map(
|
||||
.query_map_vec(
|
||||
"SELECT id, uid, target FROM imap
|
||||
WHERE folder = ?
|
||||
AND target != folder
|
||||
@@ -1082,7 +1052,6 @@ impl Session {
|
||||
let target: String = row.get(2)?;
|
||||
Ok((rowid, uid, target))
|
||||
},
|
||||
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1173,7 +1142,7 @@ impl Session {
|
||||
pub(crate) async fn store_seen_flags_on_imap(&mut self, context: &Context) -> Result<()> {
|
||||
let rows = context
|
||||
.sql
|
||||
.query_map(
|
||||
.query_map_vec(
|
||||
"SELECT imap.id, uid, folder FROM imap, imap_markseen
|
||||
WHERE imap.id = imap_markseen.id AND target = folder
|
||||
ORDER BY folder, uid",
|
||||
@@ -1184,7 +1153,6 @@ impl Session {
|
||||
let folder: String = row.get(2)?;
|
||||
Ok((rowid, uid, folder))
|
||||
},
|
||||
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1291,15 +1259,14 @@ impl Session {
|
||||
continue;
|
||||
};
|
||||
let is_seen = fetch.flags().any(|flag| flag == Flag::Seen);
|
||||
if is_seen {
|
||||
if let Some(chat_id) = mark_seen_by_uid(context, folder, uid_validity, uid)
|
||||
if is_seen
|
||||
&& let Some(chat_id) = mark_seen_by_uid(context, folder, uid_validity, uid)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("failed to update seen status for msg {folder}/{uid}")
|
||||
})?
|
||||
{
|
||||
updated_chat_ids.insert(chat_id);
|
||||
}
|
||||
{
|
||||
updated_chat_ids.insert(chat_id);
|
||||
}
|
||||
|
||||
if let Some(modseq) = fetch.modseq {
|
||||
@@ -1353,10 +1320,10 @@ impl Session {
|
||||
while let Some(msg) = list.try_next().await? {
|
||||
match get_fetch_headers(&msg) {
|
||||
Ok(headers) => {
|
||||
if let Some(from) = mimeparser::get_from(&headers) {
|
||||
if context.is_self_addr(&from.addr).await? {
|
||||
result.extend(mimeparser::get_recipients(&headers));
|
||||
}
|
||||
if let Some(from) = mimeparser::get_from(&headers)
|
||||
&& context.is_self_addr(&from.addr).await?
|
||||
{
|
||||
result.extend(mimeparser::get_recipients(&headers));
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -1383,12 +1350,10 @@ impl Session {
|
||||
///
|
||||
/// If the message is incorrect or there is a failure to write a message to the database,
|
||||
/// it is skipped and the error is logged.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
pub(crate) async fn fetch_many_msgs(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
folder: &str,
|
||||
uidvalidity: u32,
|
||||
request_uids: Vec<u32>,
|
||||
uid_message_ids: &BTreeMap<u32, String>,
|
||||
fetch_partially: bool,
|
||||
@@ -1512,35 +1477,19 @@ impl Session {
|
||||
context,
|
||||
"Passing message UID {} to receive_imf().", request_uid
|
||||
);
|
||||
let res = receive_imf_inner(
|
||||
context,
|
||||
folder,
|
||||
uidvalidity,
|
||||
request_uid,
|
||||
rfc724_mid,
|
||||
body,
|
||||
is_seen,
|
||||
partial.map(|msg_size| (msg_size, None)),
|
||||
)
|
||||
.await;
|
||||
let received_msg = if let Err(err) = res {
|
||||
warn!(context, "receive_imf error: {:#}.", err);
|
||||
if partial.is_some() {
|
||||
return Err(err);
|
||||
let res = receive_imf_inner(context, rfc724_mid, body, is_seen, partial).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/."
|
||||
);
|
||||
let mut msg = Message::new_text(text);
|
||||
add_device_msg(context, None, Some(&mut msg)).await?;
|
||||
None
|
||||
}
|
||||
receive_imf_inner(
|
||||
context,
|
||||
folder,
|
||||
uidvalidity,
|
||||
request_uid,
|
||||
rfc724_mid,
|
||||
body,
|
||||
is_seen,
|
||||
Some((body.len().try_into()?, Some(format!("{err:#}")))),
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
res?
|
||||
Ok(msg) => msg,
|
||||
};
|
||||
received_msgs_channel
|
||||
.send((request_uid, received_msg))
|
||||
@@ -1607,17 +1556,17 @@ impl Session {
|
||||
.await?;
|
||||
let mut got_turn_server = false;
|
||||
for m in metadata {
|
||||
if m.entry == "/shared/vendor/deltachat/turn" {
|
||||
if let Some(value) = m.value {
|
||||
match create_ice_servers_from_metadata(context, &value).await {
|
||||
Ok((parsed_timestamp, parsed_ice_servers)) => {
|
||||
old_metadata.ice_servers_expiration_timestamp = parsed_timestamp;
|
||||
old_metadata.ice_servers = parsed_ice_servers;
|
||||
got_turn_server = false;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Failed to parse TURN server metadata: {err:#}.");
|
||||
}
|
||||
if 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:#}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1902,11 +1851,13 @@ impl Imap {
|
||||
info!(context, "Scanning folder: {:?}", folder);
|
||||
|
||||
// Update the delimiter iff there is a different one, but only once.
|
||||
if let Some(d) = folder.delimiter() {
|
||||
if delimiter_is_default && !d.is_empty() && delimiter != d {
|
||||
delimiter = d.to_string();
|
||||
delimiter_is_default = false;
|
||||
}
|
||||
if let Some(d) = folder.delimiter()
|
||||
&& delimiter_is_default
|
||||
&& !d.is_empty()
|
||||
&& delimiter != d
|
||||
{
|
||||
delimiter = d.to_string();
|
||||
delimiter_is_default = false;
|
||||
}
|
||||
|
||||
let folder_meaning = get_folder_meaning_by_attrs(folder.attributes());
|
||||
@@ -2089,7 +2040,7 @@ async fn spam_target_folder_cfg(
|
||||
|
||||
if needs_move_to_mvbox(context, headers).await?
|
||||
// If OnlyFetchMvbox is set, we don't want to move the message to
|
||||
// the inbox or sentbox where we wouldn't fetch it again:
|
||||
// the inbox where we wouldn't fetch it again:
|
||||
|| context.get_config_bool(Config::OnlyFetchMvbox).await?
|
||||
{
|
||||
Ok(Some(Config::ConfiguredMvboxFolder))
|
||||
@@ -2098,7 +2049,7 @@ async fn spam_target_folder_cfg(
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `ConfiguredInboxFolder`, `ConfiguredMvboxFolder` or `ConfiguredSentboxFolder` if
|
||||
/// Returns `ConfiguredInboxFolder` or `ConfiguredMvboxFolder` if
|
||||
/// the message needs to be moved from `folder`. Otherwise returns `None`.
|
||||
pub async fn target_folder_cfg(
|
||||
context: &Context,
|
||||
@@ -2112,7 +2063,9 @@ pub async fn target_folder_cfg(
|
||||
|
||||
if folder_meaning == FolderMeaning::Spam {
|
||||
spam_target_folder_cfg(context, headers).await
|
||||
} else if needs_move_to_mvbox(context, headers).await? {
|
||||
} else if folder_meaning == FolderMeaning::Inbox
|
||||
&& needs_move_to_mvbox(context, headers).await?
|
||||
{
|
||||
Ok(Some(Config::ConfiguredMvboxFolder))
|
||||
} else {
|
||||
Ok(None)
|
||||
@@ -2145,12 +2098,10 @@ async fn needs_move_to_mvbox(
|
||||
.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?
|
||||
{
|
||||
if let Some(from) = mimeparser::get_from(headers) {
|
||||
if context.is_self_addr(&from.addr).await? {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
return Ok(true);
|
||||
}
|
||||
if !context.get_config_bool(Config::MvboxMove).await? {
|
||||
return Ok(false);
|
||||
@@ -2185,38 +2136,6 @@ async fn needs_move_to_mvbox(
|
||||
// but sth. different in others - a hard job.
|
||||
fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
|
||||
// source: <https://stackoverflow.com/questions/2185391/localized-gmail-imap-folders>
|
||||
const SENT_NAMES: &[&str] = &[
|
||||
"sent",
|
||||
"sentmail",
|
||||
"sent objects",
|
||||
"gesendet",
|
||||
"Sent Mail",
|
||||
"Sendte e-mails",
|
||||
"Enviados",
|
||||
"Messages envoyés",
|
||||
"Messages envoyes",
|
||||
"Posta inviata",
|
||||
"Verzonden berichten",
|
||||
"Wyslane",
|
||||
"E-mails enviados",
|
||||
"Correio enviado",
|
||||
"Enviada",
|
||||
"Enviado",
|
||||
"Gönderildi",
|
||||
"Inviati",
|
||||
"Odeslaná pošta",
|
||||
"Sendt",
|
||||
"Skickat",
|
||||
"Verzonden",
|
||||
"Wysłane",
|
||||
"Éléments envoyés",
|
||||
"Απεσταλμένα",
|
||||
"Отправленные",
|
||||
"寄件備份",
|
||||
"已发送邮件",
|
||||
"送信済み",
|
||||
"보낸편지함",
|
||||
];
|
||||
const SPAM_NAMES: &[&str] = &[
|
||||
"spam",
|
||||
"junk",
|
||||
@@ -2260,8 +2179,8 @@ fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
|
||||
];
|
||||
let lower = folder_name.to_lowercase();
|
||||
|
||||
if SENT_NAMES.iter().any(|s| s.to_lowercase() == lower) {
|
||||
FolderMeaning::Sent
|
||||
if lower == "inbox" {
|
||||
FolderMeaning::Inbox
|
||||
} else if SPAM_NAMES.iter().any(|s| s.to_lowercase() == lower) {
|
||||
FolderMeaning::Spam
|
||||
} else if TRASH_NAMES.iter().any(|s| s.to_lowercase() == lower) {
|
||||
@@ -2275,7 +2194,6 @@ fn get_folder_meaning_by_attrs(folder_attrs: &[NameAttribute]) -> FolderMeaning
|
||||
for attr in folder_attrs {
|
||||
match attr {
|
||||
NameAttribute::Trash => return FolderMeaning::Trash,
|
||||
NameAttribute::Sent => return FolderMeaning::Sent,
|
||||
NameAttribute::Junk => return FolderMeaning::Spam,
|
||||
NameAttribute::All | NameAttribute::Flagged => return FolderMeaning::Virtual,
|
||||
NameAttribute::Extension(label) => {
|
||||
@@ -2353,12 +2271,13 @@ pub(crate) async fn prefetch_should_download(
|
||||
// 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? {
|
||||
if 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);
|
||||
}
|
||||
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_) {
|
||||
@@ -2416,15 +2335,6 @@ pub(crate) async fn prefetch_should_download(
|
||||
Ok(should_download)
|
||||
}
|
||||
|
||||
/// Returns whether a message is a duplicate (resent message).
|
||||
pub(crate) fn is_dup_msg(is_chat_msg: bool, ts_sent: i64, ts_sent_old: i64) -> bool {
|
||||
// If the existing message has timestamp_sent == 0, that means we don't know its actual sent
|
||||
// timestamp, so don't delete the new message. E.g. outgoing messages have zero timestamp_sent
|
||||
// because they are stored to the db before sending. Also consider as duplicates only messages
|
||||
// with greater timestamp to avoid deleting both messages in a multi-device setting.
|
||||
is_chat_msg && ts_sent_old != 0 && ts_sent > ts_sent_old
|
||||
}
|
||||
|
||||
/// Marks messages in `msgs` table as seen, searching for them by UID.
|
||||
///
|
||||
/// Returns updated chat ID if any message was marked as seen.
|
||||
@@ -2591,21 +2501,6 @@ pub(crate) async fn get_imap_self_sent_search_command(context: &Context) -> Resu
|
||||
Ok(search_command)
|
||||
}
|
||||
|
||||
/// Deprecated, use get_uid_next() and get_uidvalidity()
|
||||
pub async fn get_config_last_seen_uid(context: &Context, folder: &str) -> Result<(u32, u32)> {
|
||||
let key = format!("imap.mailbox.{folder}");
|
||||
if let Some(entry) = context.sql.get_raw_config(&key).await? {
|
||||
// the entry has the format `imap.mailbox.<folder>=<uidvalidity>:<lastseenuid>`
|
||||
let mut parts = entry.split(':');
|
||||
Ok((
|
||||
parts.next().unwrap_or_default().parse().unwrap_or(0),
|
||||
parts.next().unwrap_or_default().parse().unwrap_or(0),
|
||||
))
|
||||
} else {
|
||||
Ok((0, 0))
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether to ignore fetching messages from a folder.
|
||||
///
|
||||
/// This caters for the [`Config::OnlyFetchMvbox`] setting which means mails from folders
|
||||
@@ -2618,10 +2513,6 @@ async fn should_ignore_folder(
|
||||
if !context.get_config_bool(Config::OnlyFetchMvbox).await? {
|
||||
return Ok(false);
|
||||
}
|
||||
if context.is_sentbox(folder).await? {
|
||||
// Still respect the SentboxWatch setting.
|
||||
return Ok(!context.get_config_bool(Config::SentboxWatch).await?);
|
||||
}
|
||||
Ok(!(context.is_mvbox(folder).await? || folder_meaning == FolderMeaning::Spam))
|
||||
}
|
||||
|
||||
@@ -2633,11 +2524,11 @@ fn build_sequence_sets(uids: &[u32]) -> Result<Vec<(Vec<u32>, String)>> {
|
||||
let mut ranges: Vec<UidRange> = vec![];
|
||||
|
||||
for ¤t in uids {
|
||||
if let Some(last) = ranges.last_mut() {
|
||||
if last.end + 1 == current {
|
||||
last.end = current;
|
||||
continue;
|
||||
}
|
||||
if let Some(last) = ranges.last_mut()
|
||||
&& last.end + 1 == current
|
||||
{
|
||||
last.end = current;
|
||||
continue;
|
||||
}
|
||||
|
||||
ranges.push(UidRange {
|
||||
|
||||
@@ -8,14 +8,15 @@ use tokio::io::BufWriter;
|
||||
|
||||
use super::capabilities::Capabilities;
|
||||
use crate::context::Context;
|
||||
use crate::log::{LoggingStream, info, warn};
|
||||
use crate::login_param::{ConnectionCandidate, ConnectionSecurity};
|
||||
use crate::log::{LoggingStream, warn};
|
||||
use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp};
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::net::session::SessionStream;
|
||||
use crate::net::tls::wrap_tls;
|
||||
use crate::net::{connect_tcp_inner, run_connection_attempts, update_connection_history};
|
||||
use crate::tools::time;
|
||||
use crate::transport::ConnectionCandidate;
|
||||
use crate::transport::ConnectionSecurity;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Client {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user