mirror of
https://github.com/chatmail/core.git
synced 2026-04-01 21:12:13 +03:00
Compare commits
454 Commits
v2.23.0
...
430cccfd32
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
430cccfd32 | ||
|
|
822a99ea9c | ||
|
|
bf02785a36 | ||
|
|
01b2aa0f66 | ||
|
|
fb46c34b55 | ||
|
|
9393753190 | ||
|
|
d9056fd187 | ||
|
|
7b17b1f8b8 | ||
|
|
d8d7f12af0 | ||
|
|
0150d38ddd | ||
|
|
11b6a108f5 | ||
|
|
54858361a9 | ||
|
|
6a705a3ef6 | ||
|
|
a23e41ea6d | ||
|
|
bdca3e5c09 | ||
|
|
a61a25f139 | ||
|
|
5404e683eb | ||
|
|
80acc9d467 | ||
|
|
3c5af7a559 | ||
|
|
f7e9973fb4 | ||
|
|
c0a3d77301 | ||
|
|
9891c2a531 | ||
|
|
f85c625799 | ||
|
|
b30f93a57d | ||
|
|
a95bf77868 | ||
|
|
d26fa715b5 | ||
|
|
1b43aac356 | ||
|
|
53acfaa054 | ||
|
|
874e38c146 | ||
|
|
cce8e3bc5a | ||
|
|
1e20055523 | ||
|
|
abb93cd79d | ||
|
|
5f84be718a | ||
|
|
d1c3a679a0 | ||
|
|
0c4e32363e | ||
|
|
89b5675b83 | ||
|
|
8ff8ba7416 | ||
|
|
e3a7d555a8 | ||
|
|
964bbad53e | ||
|
|
a1eb376131 | ||
|
|
3c4ce17f1e | ||
|
|
0622289420 | ||
|
|
c928015f20 | ||
|
|
b10acd194e | ||
|
|
b94792706a | ||
|
|
bfae2296b7 | ||
|
|
e7625ca231 | ||
|
|
ab08a47298 | ||
|
|
b85fa84a37 | ||
|
|
ccd3caf4a7 | ||
|
|
5f248954dc | ||
|
|
a6c7958739 | ||
|
|
c724e2981c | ||
|
|
ffd9f80f8b | ||
|
|
42cb9fe890 | ||
|
|
914486cb32 | ||
|
|
526b3b0271 | ||
|
|
1c439b5ef4 | ||
|
|
f97c75f146 | ||
|
|
76a36a35bf | ||
|
|
dc4249a2ff | ||
|
|
957c0b7c56 | ||
|
|
8df9b9e4d9 | ||
|
|
692e1019b0 | ||
|
|
2511b03726 | ||
|
|
c39651a8d4 | ||
|
|
8230336936 | ||
|
|
e1e8407905 | ||
|
|
ffce0dfc9a | ||
|
|
e2eec2f1f8 | ||
|
|
072c0061ee | ||
|
|
cb783ffc12 | ||
|
|
af182a85a3 | ||
|
|
7d8989a068 | ||
|
|
d7bf10d7a4 | ||
|
|
f1e90c73cd | ||
|
|
c39d2f42ef | ||
|
|
e60f4ff70a | ||
|
|
ba64d8d19b | ||
|
|
4041d9a54e | ||
|
|
bbf9a86bce | ||
|
|
cdb0e0ce29 | ||
|
|
0e7f3c8238 | ||
|
|
16c85a9585 | ||
|
|
ff7023580f | ||
|
|
58d457140e | ||
|
|
b531a3c012 | ||
|
|
f055f6226c | ||
|
|
e95dca87bd | ||
|
|
0d9442458a | ||
|
|
60cf483270 | ||
|
|
598d759b8d | ||
|
|
10b93b3943 | ||
|
|
5a06d08613 | ||
|
|
85de4bf678 | ||
|
|
624fc394d9 | ||
|
|
9deba0cf2a | ||
|
|
b95d28b2d9 | ||
|
|
2131f5e9c0 | ||
|
|
a63f695b85 | ||
|
|
de25eb90ff | ||
|
|
3fdda6f3b8 | ||
|
|
c475882727 | ||
|
|
166e259b18 | ||
|
|
cc38298163 | ||
|
|
983f43c33c | ||
|
|
5028842fd5 | ||
|
|
e78b509d0a | ||
|
|
583979c6fc | ||
|
|
5bfd8dd517 | ||
|
|
32b0ca81f8 | ||
|
|
8dd7e5c5dd | ||
|
|
5bb0b86f6a | ||
|
|
ed2b0e8f03 | ||
|
|
8152ff518e | ||
|
|
cbcfb7087e | ||
|
|
396104af47 | ||
|
|
69f6727751 | ||
|
|
b72a677f4c | ||
|
|
00e78eecf6 | ||
|
|
8b0621b724 | ||
|
|
63bf4c4f33 | ||
|
|
d6bce56d18 | ||
|
|
c8dec0dcdd | ||
|
|
509644ea5f | ||
|
|
3e95239e71 | ||
|
|
74d4b823d2 | ||
|
|
1bcfb90b90 | ||
|
|
411ee511ed | ||
|
|
e5a30c341c | ||
|
|
3d409c37a1 | ||
|
|
b46c86c9b7 | ||
|
|
e5e268f503 | ||
|
|
633536bb13 | ||
|
|
94ee485155 | ||
|
|
ec0dc8bcad | ||
|
|
49296e3014 | ||
|
|
2b93e856e4 | ||
|
|
c5be7df1d7 | ||
|
|
6b74cb6539 | ||
|
|
de2ac8cca2 | ||
|
|
085fcd2751 | ||
|
|
83f30e4a54 | ||
|
|
e79b4baa09 | ||
|
|
1e0c0d8efa | ||
|
|
378fb09c80 | ||
|
|
ff2fbebff0 | ||
|
|
50a73666fd | ||
|
|
61a8eff2ad | ||
|
|
cbd379fdf0 | ||
|
|
fe826f762e | ||
|
|
2019debe99 | ||
|
|
6c4f4bfd19 | ||
|
|
44b0736216 | ||
|
|
3b29469102 | ||
|
|
6325a35b5b | ||
|
|
c08644490a | ||
|
|
955f79923a | ||
|
|
c9026bff2c | ||
|
|
4fc0d0f53d | ||
|
|
1bf24618fa | ||
|
|
3f98e45c29 | ||
|
|
26ddcfaaed | ||
|
|
f0a12d493c | ||
|
|
c848ea7eda | ||
|
|
7c55356271 | ||
|
|
f4ee01ecca | ||
|
|
448c0d2268 | ||
|
|
3325270896 | ||
|
|
b563064b26 | ||
|
|
8d32d3ae0c | ||
|
|
c5f19f67a9 | ||
|
|
baeb31b5fa | ||
|
|
5d3bc00fd5 | ||
|
|
424928b660 | ||
|
|
1b8c732611 | ||
|
|
2531dfea1d | ||
|
|
9003b248aa | ||
|
|
35875f9b32 | ||
|
|
008e6c4af3 | ||
|
|
a6baba1852 | ||
|
|
a6b2a54e46 | ||
|
|
99aa99eb5b | ||
|
|
566395f1fa | ||
|
|
4ccd3cb665 | ||
|
|
f5e1e2678b | ||
|
|
c3a5e3ac0d | ||
|
|
b2f31c8148 | ||
|
|
29c57ad065 | ||
|
|
82a0d6b0ab | ||
|
|
5ff323ce15 | ||
|
|
a67a5299bf | ||
|
|
659d21aa9d | ||
|
|
8f604e74ec | ||
|
|
e1ebf3e96d | ||
|
|
76171aea2e | ||
|
|
96b8d1720e | ||
|
|
47b49fd02e | ||
|
|
f50e3d6ffa | ||
|
|
2ecb537307 | ||
|
|
ccae73f6db | ||
|
|
fce91f3ee0 | ||
|
|
d446a16fc6 | ||
|
|
c3a6e48882 | ||
|
|
46ec3a469b | ||
|
|
fe3b1ea16d | ||
|
|
ed300b6f97 | ||
|
|
e456be4e21 | ||
|
|
ba4055b7df | ||
|
|
c06f53cb86 | ||
|
|
13dafa46b5 | ||
|
|
d552250dc4 | ||
|
|
1383e790c3 | ||
|
|
b536902827 | ||
|
|
2631745a57 | ||
|
|
46bbe5f077 | ||
|
|
0f14edd5d9 | ||
|
|
fe6e942191 | ||
|
|
67aac12995 | ||
|
|
f2fb59f0cc | ||
|
|
55ab1b86f7 | ||
|
|
ceba687df3 | ||
|
|
7e811469b3 | ||
|
|
cdacad235e | ||
|
|
c766397abc | ||
|
|
14a59afd5d | ||
|
|
9c883e6424 | ||
|
|
9d7db20225 | ||
|
|
fdb583b5e9 | ||
|
|
8d6f4b0354 | ||
|
|
284469363e | ||
|
|
6078c79020 | ||
|
|
161e5ae358 | ||
|
|
a66859ebf2 | ||
|
|
de902babc3 | ||
|
|
98a8679779 | ||
|
|
8ce3ecc809 | ||
|
|
50a1f907a5 | ||
|
|
c2bf2a32b5 | ||
|
|
84710384fe | ||
|
|
f60fce22ed | ||
|
|
0d2f2b3266 | ||
|
|
516f0a1a98 | ||
|
|
25750de4e1 | ||
|
|
a89ce8ce7a | ||
|
|
9ac64ea6b9 | ||
|
|
294e23d82d | ||
|
|
184736723f | ||
|
|
cea528ed61 | ||
|
|
9b11f53da6 | ||
|
|
5c339efb70 | ||
|
|
d71c163c7d | ||
|
|
19fde9594f | ||
|
|
20b3a06adf | ||
|
|
b0127fa381 | ||
|
|
6a293aebe2 | ||
|
|
fd90493766 | ||
|
|
b1883c802b | ||
|
|
71ee32b8b7 | ||
|
|
84161f4202 | ||
|
|
4af9463a91 | ||
|
|
ddd4fc49a2 | ||
|
|
7d5bedde4d | ||
|
|
e34fee72a0 | ||
|
|
7ba4a43253 | ||
|
|
a09fd4577a | ||
|
|
525a3539d2 | ||
|
|
fbcdd45015 | ||
|
|
1ea8ed6442 | ||
|
|
f6817131b8 | ||
|
|
28fc1d2ff2 | ||
|
|
5925f72316 | ||
|
|
8dfa5fc37e | ||
|
|
49b04e8789 | ||
|
|
d87d87f467 | ||
|
|
bf72b3ad49 | ||
|
|
30f2981259 | ||
|
|
121bfd1fa8 | ||
|
|
9e2a4325e9 | ||
|
|
4509c1bd06 | ||
|
|
3133d89dcc | ||
|
|
99775458c4 | ||
|
|
e432960246 | ||
|
|
58cd133b5c | ||
|
|
3d234e7fc7 | ||
|
|
595258ae05 | ||
|
|
06b2a890da | ||
|
|
95ed31391d | ||
|
|
98944efdb8 | ||
|
|
3f27be9bcb | ||
|
|
5902fe2cbe | ||
|
|
73e0f81e83 | ||
|
|
cbe842735e | ||
|
|
72bc9f0ae4 | ||
|
|
0defa117a0 | ||
|
|
3821cfab0c | ||
|
|
09f159991e | ||
|
|
014d2ace76 | ||
|
|
646728372b | ||
|
|
7c30aef2ed | ||
|
|
c38d02728e | ||
|
|
dea1b414db | ||
|
|
aa5ee19340 | ||
|
|
9271ecd208 | ||
|
|
952f6735a2 | ||
|
|
a50aa3b6e9 | ||
|
|
23d95df66a | ||
|
|
6db2cf6144 | ||
|
|
47c1e54219 | ||
|
|
b41c309e21 | ||
|
|
f7ae2abe52 | ||
|
|
3a7f82c66e | ||
|
|
d75a78d446 | ||
|
|
676132457f | ||
|
|
8bce137e06 | ||
|
|
f359a9c451 | ||
|
|
0d97a5b511 | ||
|
|
7ccc021aea | ||
|
|
08e9cdc487 | ||
|
|
12cee23924 | ||
|
|
5fb118e5a3 | ||
|
|
1ec3f45dc1 | ||
|
|
e4e19b57b3 | ||
|
|
2efb128fec | ||
|
|
4a5d5bdeb1 | ||
|
|
cde4a61be7 | ||
|
|
ca2b4d7a6f | ||
|
|
ef61c0c408 | ||
|
|
dc5f939ac6 | ||
|
|
2c0092738f | ||
|
|
b1e6cf2052 | ||
|
|
343dca87f7 | ||
|
|
4d06f5a8ae | ||
|
|
a4bec7dc70 | ||
|
|
1f32c5ab40 | ||
|
|
43e8d5cc6c | ||
|
|
7e4547582e | ||
|
|
721b9cebef | ||
|
|
0d50d8703f | ||
|
|
9aba299c75 | ||
|
|
2854f87a9d | ||
|
|
145a5813e8 | ||
|
|
4cb129a67e | ||
|
|
7bf7ec3d32 | ||
|
|
8a7498a9a8 | ||
|
|
c41a69ea1e | ||
|
|
38a547dfda | ||
|
|
7c998af973 | ||
|
|
6b6ec2a4b7 | ||
|
|
b1fa1055d7 | ||
|
|
15ce05b0c7 | ||
|
|
8112183429 | ||
|
|
b9ae74fab2 | ||
|
|
fdff90eba4 | ||
|
|
a4a54d3648 | ||
|
|
98fb760f49 | ||
|
|
f553c094eb | ||
|
|
bbb4bed996 | ||
|
|
25088f2dcb | ||
|
|
552e9f4052 | ||
|
|
b3616a013f | ||
|
|
c378b1218d | ||
|
|
21f6e7c676 | ||
|
|
ac543ad251 | ||
|
|
183898b137 | ||
|
|
56204ae701 | ||
|
|
7906405400 | ||
|
|
531e0bc914 | ||
|
|
3637fe67a7 | ||
|
|
8eef79f95d | ||
|
|
6077499f07 | ||
|
|
94d2d8cfd7 | ||
|
|
ba3cad6ad6 | ||
|
|
c9c362d5ff | ||
|
|
6514b4ca7f | ||
|
|
e7e31d7914 | ||
|
|
51d6855e0d | ||
|
|
2f90b55309 | ||
|
|
be3e202470 | ||
|
|
57aadfbbf6 | ||
|
|
849cde9757 | ||
|
|
b4cd99fc56 | ||
|
|
9305a0676c | ||
|
|
39c9ba19ef | ||
|
|
af574279fd | ||
|
|
713c929e03 | ||
|
|
c83c131a37 | ||
|
|
0d0602a4a5 | ||
|
|
abfb556377 | ||
|
|
72788daca0 | ||
|
|
16bd87c78f | ||
|
|
d44e2420bc | ||
|
|
88d213fcdb | ||
|
|
fb14acb0fb | ||
|
|
a5c470fbae | ||
|
|
6bdba33d32 | ||
|
|
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 |
100
.github/workflows/ci.yml
vendored
100
.github/workflows/ci.yml
vendored
@@ -20,17 +20,18 @@ permissions: {}
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
RUST_VERSION: 1.91.0
|
||||
RUST_VERSION: 1.94.0
|
||||
|
||||
# Minimum Supported Rust Version
|
||||
MSRV: 1.85.0
|
||||
MSRV: 1.88.0
|
||||
|
||||
jobs:
|
||||
lint_rust:
|
||||
name: Lint Rust
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -39,7 +40,7 @@ jobs:
|
||||
- run: rustup override set $RUST_VERSION
|
||||
shell: bash
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v2
|
||||
uses: swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
|
||||
- name: Run rustfmt
|
||||
run: cargo fmt --all -- --check
|
||||
- name: Run clippy
|
||||
@@ -52,22 +53,24 @@ jobs:
|
||||
cargo_deny:
|
||||
name: cargo deny
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
- uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979
|
||||
with:
|
||||
arguments: --all-features --workspace
|
||||
arguments: --workspace --all-features --locked
|
||||
command: check
|
||||
command-arguments: "-Dwarnings"
|
||||
|
||||
provider_database:
|
||||
name: Check provider database
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -79,15 +82,16 @@ jobs:
|
||||
docs:
|
||||
name: Rust doc comments
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
RUSTDOCFLAGS: -Dwarnings
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v2
|
||||
uses: swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
|
||||
- name: Rustdoc
|
||||
run: cargo doc --document-private-items --no-deps
|
||||
|
||||
@@ -107,6 +111,7 @@ jobs:
|
||||
- os: ubuntu-latest
|
||||
rust: minimum
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- run:
|
||||
echo "RUSTUP_TOOLCHAIN=$MSRV" >> $GITHUB_ENV
|
||||
@@ -117,7 +122,7 @@ jobs:
|
||||
shell: bash
|
||||
if: matrix.rust == 'latest'
|
||||
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -129,10 +134,10 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v2
|
||||
uses: swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@v2
|
||||
uses: taiki-e/install-action@69e777b377e4ec209ddad9426ae3e0c1008b0ef3
|
||||
with:
|
||||
tool: nextest
|
||||
|
||||
@@ -155,20 +160,21 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v2
|
||||
uses: swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
|
||||
|
||||
- name: Build C library
|
||||
run: cargo build -p deltachat_ffi
|
||||
|
||||
- name: Upload C library
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ matrix.os }}-libdeltachat.a
|
||||
path: target/debug/libdeltachat.a
|
||||
@@ -180,20 +186,21 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v2
|
||||
uses: swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
|
||||
|
||||
- name: Build deltachat-rpc-server
|
||||
run: cargo build -p deltachat-rpc-server
|
||||
|
||||
- name: Upload deltachat-rpc-server
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ matrix.os }}-deltachat-rpc-server
|
||||
path: ${{ matrix.os == 'windows-latest' && 'target/debug/deltachat-rpc-server.exe' || 'target/debug/deltachat-rpc-server' }}
|
||||
@@ -202,8 +209,9 @@ jobs:
|
||||
python_lint:
|
||||
name: Python lint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -219,6 +227,38 @@ jobs:
|
||||
working-directory: deltachat-rpc-client
|
||||
run: tox -e lint
|
||||
|
||||
# mypy does not work with PyPy since mypy 1.19
|
||||
# as it introduced native `librt` dependency
|
||||
# that uses CPython internals.
|
||||
# We only run mypy with CPython because of this.
|
||||
cffi_python_mypy:
|
||||
name: CFFI Python mypy
|
||||
needs: ["c_library", "python_lint"]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download libdeltachat.a
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: ubuntu-latest-libdeltachat.a
|
||||
path: target/debug
|
||||
|
||||
- name: Install tox
|
||||
run: pip install tox
|
||||
|
||||
- name: Run mypy
|
||||
env:
|
||||
DCC_RS_TARGET: debug
|
||||
DCC_RS_DEV: ${{ github.workspace }}
|
||||
working-directory: python
|
||||
run: tox -e mypy
|
||||
|
||||
|
||||
cffi_python_tests:
|
||||
name: CFFI Python tests
|
||||
needs: ["c_library", "python_lint"]
|
||||
@@ -238,21 +278,22 @@ jobs:
|
||||
- os: macos-latest
|
||||
python: pypy3.10
|
||||
|
||||
# Minimum Supported Python Version = 3.8
|
||||
# Minimum Supported Python Version = 3.10
|
||||
# This is the minimum version for which manylinux Python wheels are
|
||||
# built. Test it with minimum supported Rust version.
|
||||
- os: ubuntu-latest
|
||||
python: 3.8
|
||||
python: "3.10"
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download libdeltachat.a
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.os }}-libdeltachat.a
|
||||
path: target/debug
|
||||
@@ -271,7 +312,7 @@ jobs:
|
||||
DCC_RS_TARGET: debug
|
||||
DCC_RS_DEV: ${{ github.workspace }}
|
||||
working-directory: python
|
||||
run: tox -e mypy,doc,py
|
||||
run: tox -e doc,py
|
||||
|
||||
rpc_python_tests:
|
||||
name: JSON-RPC Python tests
|
||||
@@ -293,13 +334,14 @@ jobs:
|
||||
- os: macos-latest
|
||||
python: pypy3.10
|
||||
|
||||
# Minimum Supported Python Version = 3.8
|
||||
# Minimum Supported Python Version = 3.10
|
||||
- os: ubuntu-latest
|
||||
python: 3.8
|
||||
python: "3.10"
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -313,7 +355,7 @@ jobs:
|
||||
run: pip install tox
|
||||
|
||||
- name: Download deltachat-rpc-server
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.os }}-deltachat-rpc-server
|
||||
path: target/debug
|
||||
|
||||
253
.github/workflows/deltachat-rpc-server.yml
vendored
253
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -30,22 +30,46 @@ jobs:
|
||||
arch: [aarch64, armv7l, armv6l, i686, x86_64]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
|
||||
- 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@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||
path: result/bin/deltachat-rpc-server
|
||||
if-no-files-found: error
|
||||
|
||||
build_linux_wheel:
|
||||
name: Linux wheel
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [aarch64, armv7l, armv6l, i686, x86_64]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
|
||||
|
||||
- name: Upload wheel
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
|
||||
path: result/*.whl
|
||||
if-no-files-found: error
|
||||
|
||||
build_windows:
|
||||
name: Windows
|
||||
strategy:
|
||||
@@ -54,22 +78,46 @@ jobs:
|
||||
arch: [win32, win64]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
|
||||
- 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@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}
|
||||
path: result/bin/deltachat-rpc-server.exe
|
||||
if-no-files-found: error
|
||||
|
||||
build_windows_wheel:
|
||||
name: Windows wheel
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [win32, win64]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-wheel
|
||||
|
||||
- name: Upload wheel
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-wheel
|
||||
path: result/*.whl
|
||||
if-no-files-found: error
|
||||
|
||||
build_macos:
|
||||
name: macOS
|
||||
strategy:
|
||||
@@ -79,7 +127,7 @@ jobs:
|
||||
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -91,7 +139,7 @@ jobs:
|
||||
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.arch }}-apple-darwin --features vendored
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-macos
|
||||
path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server
|
||||
@@ -105,25 +153,49 @@ jobs:
|
||||
arch: [arm64-v8a, armeabi-v7a]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
|
||||
- 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@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-android
|
||||
path: result/bin/deltachat-rpc-server
|
||||
if-no-files-found: error
|
||||
|
||||
build_android_wheel:
|
||||
name: Android wheel
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [arm64-v8a, armeabi-v7a]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android-wheel
|
||||
|
||||
- name: Upload wheel
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-android-wheel
|
||||
path: result/*.whl
|
||||
if-no-files-found: error
|
||||
|
||||
publish:
|
||||
name: Build wheels and upload binaries to the release
|
||||
needs: ["build_linux", "build_windows", "build_macos"]
|
||||
needs: ["build_linux", "build_linux_wheel", "build_windows", "build_windows_wheel", "build_macos", "build_android", "build_android_wheel"]
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/p/deltachat-rpc-server
|
||||
@@ -132,78 +204,132 @@ jobs:
|
||||
contents: write
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
|
||||
- name: Download Linux aarch64 binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-aarch64-linux
|
||||
path: deltachat-rpc-server-aarch64-linux.d
|
||||
|
||||
- name: Download Linux aarch64 wheel
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-aarch64-linux-wheel
|
||||
path: deltachat-rpc-server-aarch64-linux-wheel.d
|
||||
|
||||
- name: Download Linux armv7l binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-armv7l-linux
|
||||
path: deltachat-rpc-server-armv7l-linux.d
|
||||
|
||||
- name: Download Linux armv7l wheel
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-armv7l-linux-wheel
|
||||
path: deltachat-rpc-server-armv7l-linux-wheel.d
|
||||
|
||||
- name: Download Linux armv6l binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-armv6l-linux
|
||||
path: deltachat-rpc-server-armv6l-linux.d
|
||||
|
||||
- name: Download Linux armv6l wheel
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-armv6l-linux-wheel
|
||||
path: deltachat-rpc-server-armv6l-linux-wheel.d
|
||||
|
||||
- name: Download Linux i686 binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-i686-linux
|
||||
path: deltachat-rpc-server-i686-linux.d
|
||||
|
||||
- name: Download Linux i686 wheel
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-i686-linux-wheel
|
||||
path: deltachat-rpc-server-i686-linux-wheel.d
|
||||
|
||||
- name: Download Linux x86_64 binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64-linux
|
||||
path: deltachat-rpc-server-x86_64-linux.d
|
||||
|
||||
- name: Download Linux x86_64 wheel
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64-linux-wheel
|
||||
path: deltachat-rpc-server-x86_64-linux-wheel.d
|
||||
|
||||
- name: Download Win32 binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-win32
|
||||
path: deltachat-rpc-server-win32.d
|
||||
|
||||
- name: Download Win32 wheel
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-win32-wheel
|
||||
path: deltachat-rpc-server-win32-wheel.d
|
||||
|
||||
- name: Download Win64 binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-win64
|
||||
path: deltachat-rpc-server-win64.d
|
||||
|
||||
- name: Download Win64 wheel
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-win64-wheel
|
||||
path: deltachat-rpc-server-win64-wheel.d
|
||||
|
||||
- name: Download macOS binary for x86_64
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64-macos
|
||||
path: deltachat-rpc-server-x86_64-macos.d
|
||||
|
||||
- name: Download macOS binary for aarch64
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-aarch64-macos
|
||||
path: deltachat-rpc-server-aarch64-macos.d
|
||||
|
||||
- name: Download Android binary for arm64-v8a
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-arm64-v8a-android
|
||||
path: deltachat-rpc-server-arm64-v8a-android.d
|
||||
|
||||
- name: Download Android wheel for arm64-v8a
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-arm64-v8a-android-wheel
|
||||
path: deltachat-rpc-server-arm64-v8a-android-wheel.d
|
||||
|
||||
- name: Download Android binary for armeabi-v7a
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-armeabi-v7a-android
|
||||
path: deltachat-rpc-server-armeabi-v7a-android.d
|
||||
|
||||
- name: Download Android wheel for armeabi-v7a
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-armeabi-v7a-android-wheel
|
||||
path: deltachat-rpc-server-armeabi-v7a-android-wheel.d
|
||||
|
||||
- name: Create bin/ directory
|
||||
run: |
|
||||
mkdir -p bin
|
||||
@@ -222,38 +348,21 @@ jobs:
|
||||
- name: List binaries
|
||||
run: ls -l bin/
|
||||
|
||||
# Python 3.11 is needed for tomllib used in scripts/wheel-rpc-server.py
|
||||
- name: Install python 3.12
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.12
|
||||
|
||||
- name: Install wheel
|
||||
run: pip install wheel
|
||||
|
||||
- name: Build deltachat-rpc-server Python wheels and source package
|
||||
- name: Build deltachat-rpc-server Python wheels
|
||||
run: |
|
||||
mkdir -p dist
|
||||
nix build .#deltachat-rpc-server-x86_64-linux-wheel
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-armv7l-linux-wheel
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-armv6l-linux-wheel
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-aarch64-linux-wheel
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-i686-linux-wheel
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-win64-wheel
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-win32-wheel
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-arm64-v8a-android-wheel
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-armeabi-v7a-android-wheel
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-source
|
||||
cp result/*.tar.gz dist/
|
||||
mv deltachat-rpc-server-aarch64-linux-wheel.d/*.whl dist/
|
||||
mv deltachat-rpc-server-armv7l-linux-wheel.d/*.whl dist/
|
||||
mv deltachat-rpc-server-armv6l-linux-wheel.d/*.whl dist/
|
||||
mv deltachat-rpc-server-i686-linux-wheel.d/*.whl dist/
|
||||
mv deltachat-rpc-server-x86_64-linux-wheel.d/*.whl dist/
|
||||
mv deltachat-rpc-server-win64-wheel.d/*.whl dist/
|
||||
mv deltachat-rpc-server-win32-wheel.d/*.whl dist/
|
||||
mv deltachat-rpc-server-arm64-v8a-android-wheel.d/*.whl dist/
|
||||
mv deltachat-rpc-server-armeabi-v7a-android-wheel.d/*.whl dist/
|
||||
python3 scripts/wheel-rpc-server.py x86_64-darwin bin/deltachat-rpc-server-x86_64-macos
|
||||
python3 scripts/wheel-rpc-server.py aarch64-darwin bin/deltachat-rpc-server-aarch64-macos
|
||||
mv *.whl dist/
|
||||
@@ -271,21 +380,24 @@ jobs:
|
||||
--repo ${{ github.repository }} \
|
||||
bin/* dist/*
|
||||
|
||||
- name: Publish deltachat-rpc-client to PyPI
|
||||
- name: Publish deltachat-rpc-server to PyPI
|
||||
if: github.event_name == 'release'
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e
|
||||
|
||||
publish_npm_package:
|
||||
name: Build & Publish npm prebuilds and deltachat-rpc-server
|
||||
needs: ["build_linux", "build_windows", "build_macos"]
|
||||
runs-on: "ubuntu-latest"
|
||||
environment:
|
||||
name: npm-stdio-rpc-server
|
||||
url: https://www.npmjs.com/package/@deltachat/stdio-rpc-server
|
||||
permissions:
|
||||
id-token: write
|
||||
|
||||
# Needed to publish the binaries to the release.
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -294,67 +406,67 @@ jobs:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Download Linux aarch64 binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-aarch64-linux
|
||||
path: deltachat-rpc-server-aarch64-linux.d
|
||||
|
||||
- name: Download Linux armv7l binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-armv7l-linux
|
||||
path: deltachat-rpc-server-armv7l-linux.d
|
||||
|
||||
- name: Download Linux armv6l binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-armv6l-linux
|
||||
path: deltachat-rpc-server-armv6l-linux.d
|
||||
|
||||
- name: Download Linux i686 binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-i686-linux
|
||||
path: deltachat-rpc-server-i686-linux.d
|
||||
|
||||
- name: Download Linux x86_64 binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64-linux
|
||||
path: deltachat-rpc-server-x86_64-linux.d
|
||||
|
||||
- name: Download Win32 binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-win32
|
||||
path: deltachat-rpc-server-win32.d
|
||||
|
||||
- name: Download Win64 binary
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-win64
|
||||
path: deltachat-rpc-server-win64.d
|
||||
|
||||
- name: Download macOS binary for x86_64
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64-macos
|
||||
path: deltachat-rpc-server-x86_64-macos.d
|
||||
|
||||
- name: Download macOS binary for aarch64
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-aarch64-macos
|
||||
path: deltachat-rpc-server-aarch64-macos.d
|
||||
|
||||
- name: Download Android binary for arm64-v8a
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-arm64-v8a-android
|
||||
path: deltachat-rpc-server-arm64-v8a-android.d
|
||||
|
||||
- name: Download Android binary for armeabi-v7a
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: deltachat-rpc-server-armeabi-v7a-android
|
||||
path: deltachat-rpc-server-armeabi-v7a-android.d
|
||||
@@ -384,7 +496,7 @@ jobs:
|
||||
ls -lah
|
||||
|
||||
- name: Upload to artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: deltachat-rpc-server-npm-package
|
||||
path: deltachat-rpc-server/npm-package/*.tgz
|
||||
@@ -406,11 +518,14 @@ jobs:
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
# Ensure npm 11.5.1 or later is installed.
|
||||
# It is needed for <https://docs.npmjs.com/trusted-publishers>
|
||||
- name: Update npm
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Publish npm packets for prebuilds and `@deltachat/stdio-rpc-server`
|
||||
if: github.event_name == 'release'
|
||||
working-directory: deltachat-rpc-server/npm-package
|
||||
run: |
|
||||
ls -lah platform_package
|
||||
for platform in *.tgz; do npm publish --provenance "$platform" --access public; done
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
12
.github/workflows/jsonrpc-client-npm-package.yml
vendored
12
.github/workflows/jsonrpc-client-npm-package.yml
vendored
@@ -10,11 +10,14 @@ jobs:
|
||||
pack-module:
|
||||
name: "Publish @deltachat/jsonrpc-client"
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: npm-jsonrpc-client
|
||||
url: https://www.npmjs.com/package/@deltachat/jsonrpc-client
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -24,6 +27,11 @@ jobs:
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
# Ensure npm 11.5.1 or later is installed.
|
||||
# It is needed for <https://docs.npmjs.com/trusted-publishers>
|
||||
- name: Update npm
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Install dependencies without running scripts
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm install --ignore-scripts
|
||||
@@ -37,5 +45,3 @@ jobs:
|
||||
- name: Publish
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm publish --provenance deltachat-jsonrpc-client-* --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
4
.github/workflows/jsonrpc.yml
vendored
4
.github/workflows/jsonrpc.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
build_and_test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
with:
|
||||
node-version: 18.x
|
||||
- name: Add Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
|
||||
- name: npm install
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm install
|
||||
|
||||
12
.github/workflows/nix.yml
vendored
12
.github/workflows/nix.yml
vendored
@@ -21,11 +21,11 @@ jobs:
|
||||
name: check flake formatting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- run: nix fmt flake.nix -- --check
|
||||
|
||||
build:
|
||||
@@ -80,11 +80,11 @@ jobs:
|
||||
#- deltachat-rpc-server-x86_64-android
|
||||
#- deltachat-rpc-server-x86-android
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
build-macos:
|
||||
@@ -101,9 +101,9 @@ jobs:
|
||||
# because of <https://github.com/NixOS/nixpkgs/issues/413910>.
|
||||
# - deltachat-rpc-server-aarch64-darwin
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -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@v6
|
||||
with:
|
||||
name: python-package-distributions
|
||||
path: deltachat-rpc-client/dist/
|
||||
@@ -42,9 +42,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Download all the dists
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: python-package-distributions
|
||||
path: dist/
|
||||
- name: Publish deltachat-rpc-client to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e
|
||||
|
||||
6
.github/workflows/repl.yml
vendored
6
.github/workflows/repl.yml
vendored
@@ -14,15 +14,15 @@ jobs:
|
||||
name: Build REPL example
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- name: Build
|
||||
run: nix build .#deltachat-repl-win64
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: repl.exe
|
||||
path: "result/bin/deltachat-repl.exe"
|
||||
|
||||
12
.github/workflows/upload-docs.yml
vendored
12
.github/workflows/upload-docs.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
@@ -31,12 +31,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- name: Build Python documentation
|
||||
run: nix build .#python-docs
|
||||
- name: Upload to py.delta.chat
|
||||
@@ -50,12 +50,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- name: Build C documentation
|
||||
run: nix build .#docs
|
||||
- name: Upload to c.delta.chat
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
working-directory: ./deltachat-jsonrpc/typescript
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
2
.github/workflows/upload-ffi-docs.yml
vendored
2
.github/workflows/upload-ffi-docs.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
21
.github/workflows/zizmor-scan.yml
vendored
21
.github/workflows/zizmor-scan.yml
vendored
@@ -6,26 +6,21 @@ on:
|
||||
pull_request:
|
||||
branches: ["**"]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
zizmor:
|
||||
name: zizmor latest via PyPI
|
||||
name: Run zizmor
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
|
||||
contents: read
|
||||
actions: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d
|
||||
|
||||
- name: Run zizmor
|
||||
run: uvx zizmor --format sarif . > results.sarif
|
||||
|
||||
- name: Upload SARIF file
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
category: zizmor
|
||||
uses: zizmorcore/zizmor-action@0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d # v0.5.0
|
||||
|
||||
6
.github/zizmor.yml
vendored
Normal file
6
.github/zizmor.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
rules:
|
||||
unpinned-uses:
|
||||
config:
|
||||
policies:
|
||||
actions/*: ref-pin
|
||||
dependabot/*: ref-pin
|
||||
850
CHANGELOG.md
850
CHANGELOG.md
@@ -1,5 +1,833 @@
|
||||
# Changelog
|
||||
|
||||
## [2.45.0] - 2026-03-14
|
||||
|
||||
### API-Changes
|
||||
|
||||
- JSON-RPC: add `createQrSvg` ([#7949](https://github.com/chatmail/core/pull/7949)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Do not read own public key from the database.
|
||||
- Securejoin v3, encrypt all securejoin messages ([#7754](https://github.com/chatmail/core/pull/7754)).
|
||||
- Domain separation between securejoin auth tokens and broadcast channel secrets ([#7981](https://github.com/chatmail/core/pull/7981)).
|
||||
- Merge OpenPGP certificates and distribute relays in them.
|
||||
- Advertise SEIPDv2 feature for new keys.
|
||||
- Don't depend on cleartext `Chat-Version`, `In-Reply-To`, and `References` headers for `prefetch_should_download` ([#7932](https://github.com/chatmail/core/pull/7932)).
|
||||
- Don't send unencrypted `In-Reply-To` and `References` headers ([#7935](https://github.com/chatmail/core/pull/7935)).
|
||||
- Don't send unencrypted `Auto-Submitted` header ([#7938](https://github.com/chatmail/core/pull/7938)).
|
||||
- Remove QR code tokens sync compatibility code.
|
||||
- Mutex to prevent fetching from multiple IMAP servers at the same time.
|
||||
- Add support to gif stickers ([#7941](https://github.com/chatmail/core/pull/7941))
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix the deadlock by adding a mutex around `wal_checkpoint()`.
|
||||
- Do not run more than one housekeeping at a time.
|
||||
- ffi: don't steal Arc in `dc_jsonrpc_init` ([#7962](https://github.com/chatmail/core/pull/7962)).
|
||||
- Handle the case that the user starts a securejoin, and then deletes the contact ([#7883](https://github.com/chatmail/core/pull/7883)).
|
||||
- Do not trash pre-message if it is received twice.
|
||||
- Set `is_chatmail` during initial configuration.
|
||||
- vCard: Improve property value escaping ([#7931](https://github.com/chatmail/core/pull/7931)).
|
||||
- Percent-decode the address in `dclogin://` URLs.
|
||||
- Make broadcast owner and subscriber hidden contacts for each other ([#7856](https://github.com/chatmail/core/pull/7856)).
|
||||
- Set proper placeholder texts for system messages ([#7953](https://github.com/chatmail/core/pull/7953)).
|
||||
- Add "member added" messages to `OutBroadcast` when executing `SetPgpContacts` sync message ([#7952](https://github.com/chatmail/core/pull/7952)).
|
||||
- Correct channel system messages ([#7959](https://github.com/chatmail/core/pull/7959)).
|
||||
- Drop messages encrypted with the wrong symmetric secret ([#7963](https://github.com/chatmail/core/pull/7963)).
|
||||
- Fix debug assert message incorrectly talking about past members in the current member branch.
|
||||
- Update device chats at the end of configuration.
|
||||
- `deltachat_rpc_client`: make `@futuremethod` decorator keep method metadata.
|
||||
- Use the correct chat description stock string again ([#7939](https://github.com/chatmail/core/pull/7939)).
|
||||
- Use correct string for encryption info.
|
||||
|
||||
### CI
|
||||
|
||||
- Update Rust to 1.94.0.
|
||||
- Allow non-hash references for `actions/*` and `dependabot/*`.
|
||||
- update zizmor workflow to use zizmorcore/zizmor-action.
|
||||
|
||||
### Documentation
|
||||
|
||||
- update `store_self_keypair()` documentation.
|
||||
- Fix documentation for membership change stock strings ([#7944](https://github.com/chatmail/core/pull/7944)).
|
||||
- use correct define for 'description changed' info message.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Un-resultify `KeyPair::new()`.
|
||||
- Remove `KeyPair` type.
|
||||
- pgp: do not use legacy key ID except for IssuerKeyId subpacket.
|
||||
- `use super::*` in qr::dclogin_scheme.
|
||||
- Move WAL checkpointing into `sql::pool` submodule.
|
||||
- Order self addresses by addition timestamp.
|
||||
|
||||
### Tests
|
||||
|
||||
- Remove arbitrary timeouts from `test_4_lowlevel.py`.
|
||||
- Fix flaky `test_qr_securejoin_broadcast` ([#7937](https://github.com/chatmail/core/pull/7937)).
|
||||
- Work around `test_sync_broadcast_and_send_message` flakiness.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- bump version to 2.44.0-dev.
|
||||
- cargo: bump futures from 0.3.31 to 0.3.32.
|
||||
- cargo: bump quick-xml from 0.39.0 to 0.39.2.
|
||||
- cargo: bump criterion from 0.8.1 to 0.8.2.
|
||||
- cargo: bump tempfile from 3.24.0 to 3.25.0.
|
||||
- cargo: bump async-imap from 0.11.1 to 0.11.2.
|
||||
- cargo: bump regex from 1.12.2 to 1.12.3.
|
||||
- cargo: bump hyper-util from 0.1.19 to 0.1.20.
|
||||
- cargo: bump anyhow from 1.0.100 to 1.0.102.
|
||||
- cargo: bump syn from 2.0.114 to 2.0.117.
|
||||
- cargo: bump proptest from 1.9.0 to 1.10.0.
|
||||
- cargo: bump strum from 0.27.2 to 0.28.0.
|
||||
- cargo: bump strum_macros from 0.27.2 to 0.28.0.
|
||||
- cargo: bump quinn-proto from 0.11.9 to 0.11.14.
|
||||
|
||||
## [2.44.0] - 2026-02-27
|
||||
|
||||
### Build system
|
||||
|
||||
- git-cliff: do not capitalize the first letter of commit message.
|
||||
|
||||
### Documentation
|
||||
|
||||
- RELEASE.md: add section about dealing with antivirus false positives.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- improve logging of connection failures.
|
||||
- add backup versions to the importing error message.
|
||||
- add context to message loading failures.
|
||||
- Add 📱 to all webxdc summaries ([#7790](https://github.com/chatmail/core/pull/7790)).
|
||||
- Send webxdc name instead of raw file name in pre-messages. Display it in summary ([#7790](https://github.com/chatmail/core/pull/7790)).
|
||||
- rpc: add startup health-check and propagate server errors.
|
||||
|
||||
### Fixes
|
||||
|
||||
- imex: do not call `set_config` before running SQL migrations ([#7851](https://github.com/chatmail/core/pull/7851)).
|
||||
- add missing group description strings to cffi.
|
||||
- chat-description-changed text in old clients ([#7870](https://github.com/chatmail/core/pull/7870)).
|
||||
- add cffi type for "Description changed" info message.
|
||||
- If there was no chat description, and it's set to be an empty string, don't send out a "chat description changed" message ([#7879](https://github.com/chatmail/core/pull/7879)).
|
||||
- Make clicking on broadcast member-added messages work always ([#7882](https://github.com/chatmail/core/pull/7882)).
|
||||
- tolerate empty existing directory in Accounts::new() ([#7886](https://github.com/chatmail/core/pull/7886)).
|
||||
- If importing a backup fails, delete the partially-imported profile ([#7885](https://github.com/chatmail/core/pull/7885)).
|
||||
- Don't generate new timestamp for re-sent messages ([#7889](https://github.com/chatmail/core/pull/7889)).
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: update async-native-tls from 0.5.0 to 0.6.0.
|
||||
- add dev-version bump instructions to RELEASE.md (bumping to 2.44.0-dev).
|
||||
- deps: bump cachix/install-nix-action from 31.9.0 to 31.9.1.
|
||||
|
||||
### Performance
|
||||
|
||||
- batched event reception.
|
||||
|
||||
### Refactor
|
||||
|
||||
- enable clippy::arithmetic_side_effects lint.
|
||||
- imex: check for overflow when adding blob size.
|
||||
- http: saturating addition to calculate cache expiration timestamp.
|
||||
- Move migrations to the end of the file ([#7895](https://github.com/chatmail/core/pull/7895)).
|
||||
- do not chain Autocrypt key verification to parsing.
|
||||
|
||||
### Tests
|
||||
|
||||
- fail fast when CHATMAIL_DOMAIN is unset.
|
||||
|
||||
## [2.43.0] - 2026-02-17
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Group and broadcast channel descriptions ([#7829](https://github.com/chatmail/core/pull/7829)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Assign iroh gossip topic to pre-message when post-message is received.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Update fast-socks5 to version 1.0.
|
||||
- cargo: Update keccak from 0.1.5 to 0.1.6.
|
||||
- deps: Bump astral-sh/setup-uv from 7.1.6 to 7.3.0.
|
||||
|
||||
### Performance
|
||||
|
||||
- Use recv_direct() instead of recv() on the event channel.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Enable `clippy::manual_is_variant_and`.
|
||||
|
||||
### Tests
|
||||
|
||||
- Fix flaky `test_transport_synchronization` ([#7850](https://github.com/chatmail/core/pull/7850)).
|
||||
|
||||
## [2.42.0] - 2026-02-10
|
||||
|
||||
### Fixes
|
||||
|
||||
- Set `mvbox_move` to '0' explicitly for existing chatmail profiles.
|
||||
It's needed to prevent device message about deprecated `mvbox_move` option from appearing in chatmail profiles.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Do not scan not watched folders.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Update rPGP from 0.18.0 to 0.19.0.
|
||||
- cargo: Bump quick-xml from 0.38.4 to 0.39.0.
|
||||
|
||||
### Tests
|
||||
|
||||
- Remove test_dont_show_emails.
|
||||
|
||||
### Other
|
||||
|
||||
- Fix typo in CHANGELOG for marknoticed_all_chats.
|
||||
|
||||
## [2.41.0] - 2026-02-06
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Do not require `ShowEmails` to be set to `All` for adding second relay.
|
||||
- Use different strings for audio and video calls.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Don't set download state to Failure if message is available on another Session's transport ([#7684](https://github.com/chatmail/core/pull/7684)).
|
||||
- Make use of call stock strings.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Bump `time` from 0.3.37 to 0.3.47.
|
||||
|
||||
## [2.40.0] - 2026-02-04
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Receive_imf: Log reasoning for chat assignment.
|
||||
- Use more fitting encryption info message.
|
||||
- Send Intended Recipient Fingerprint subpackets.
|
||||
- Trash messages with intended recipient fingerprints, but w/o our one included.
|
||||
- Do not collect email addresses from messages after configuration.
|
||||
- Add device message about legacy `mvbox_move`.
|
||||
- Never create IMAP folders.
|
||||
- Make summary for pre-messages look like summary for fully downloaded messages ([#7775](https://github.com/chatmail/core/pull/7775)).
|
||||
- Don't call `BlobObject::create_and_deduplicate()` when forwarding message to the same account.
|
||||
- Allow clients to specify whether a call has video initially or not ([#7740](https://github.com/chatmail/core/pull/7740)).
|
||||
- Do not load more than one own key from the keychain.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Cross-account forwarding of a message which `has_html()` ([#7791](https://github.com/chatmail/core/pull/7791)).
|
||||
- Make self-contact a key-contact even if key isn't generated yet.
|
||||
- `apply_group_changes()`: Check whether From is key-contact.
|
||||
- Don't add SELF to unencrypted chat created from encrypted message ([#7661](https://github.com/chatmail/core/pull/7661)).
|
||||
- Don't upscale images and test that image resolution isn't changed unnecessarily ([#7769](https://github.com/chatmail/core/pull/7769)).
|
||||
- Restart i/o when there are new transports in a sync message ([#7640](https://github.com/chatmail/core/pull/7640)).
|
||||
- `add_or_lookup_key_contacts*()`: Advance fingerprint_iter on invalid address.
|
||||
- `receive_imf`: Look up key contact by intended recipient fingerprint ([#7661](https://github.com/chatmail/core/pull/7661)).
|
||||
- Remove `Config::DeleteToTrash` and `Config::ConfiguredTrashFolder`.
|
||||
|
||||
### API-Changes
|
||||
|
||||
- jsonrpc(python): Process events forever by default.
|
||||
|
||||
### CI
|
||||
|
||||
- Make scripts/deny.sh test the locked version of dependencies.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove unneeded dbg! statements ([#7776](https://github.com/chatmail/core/pull/7776)).
|
||||
- Remove unused Context.is_inbox().
|
||||
- Rename lookup_key_contacts_by_address_list() to lookup_key_contacts_fallback_to_chat().
|
||||
- Mark `ProviderOptions` as `non_exhaustive`.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Update provider database.
|
||||
- cargo: Update `bytes` from 1.11.0 to 1.11.1.
|
||||
- cargo: Bump tokio from 1.48.0 to 1.49.0.
|
||||
- cargo: Bump tokio-util from 0.7.17 to 0.7.18.
|
||||
- cargo: Bump libc from 0.2.178 to 0.2.180.
|
||||
- cargo: Bump quote from 1.0.42 to 1.0.44.
|
||||
- cargo: Bump syn from 2.0.111 to 2.0.114.
|
||||
- cargo: Bump human-panic from 2.0.4 to 2.0.6.
|
||||
- cargo: Bump chrono from 0.4.42 to 0.4.43.
|
||||
- cargo: Bump data-encoding from 2.9.0 to 2.10.0.
|
||||
- cargo: Bump colorutils-rs from 0.7.5 to 0.7.6.
|
||||
- Update provider database.
|
||||
- cargo: Bump thiserror from 2.0.17 to 2.0.18.
|
||||
- deps: Bump EmbarkStudios/cargo-deny-action from 2.0.14 to 2.0.15.
|
||||
- Remove RUSTSEC-2026-0002 exception from deny.toml.
|
||||
- cargo: Bump tokio-stream from 0.1.17 to 0.1.18.
|
||||
- cargo: Bump toml from 0.9.10+spec-1.1.0 to 0.9.11+spec-1.1.0.
|
||||
- cargo: Bump serde_json from 1.0.148 to 1.0.149.
|
||||
- cargo: Bump uuid from 1.19.0 to 1.20.0.
|
||||
- cargo: Bump rustls-pki-types from 1.13.2 to 1.14.0.
|
||||
- cargo: Bump tracing-subscriber from 0.3.20 to 0.3.22.
|
||||
|
||||
### Tests
|
||||
|
||||
- 2nd device receives message via new primary transport.
|
||||
- Make `test_dont_move_sync_msgs` less flaky.
|
||||
- Encrypted incoming message goes to encrypted 1:1 chat even if references messages in ad-hoc group.
|
||||
- Message in blocked chat arrives as InSeen.
|
||||
- Set `mvbox_move` to 0 for test rust accounts.
|
||||
|
||||
## [2.39.0] - 2026-01-23
|
||||
|
||||
### CI
|
||||
|
||||
- Update Rust to 1.93.0.
|
||||
|
||||
### Documentation
|
||||
|
||||
- RELEASE.md: Push preparation commit to the main branch before tagging.
|
||||
- RELEASE.md: Add section about dealing with failed releases.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Forward message with file ([#7755](https://github.com/chatmail/core/pull/7755)).
|
||||
- Do not additionally reduce the resolution of images that fit into the resolution-limit and are larger than the file-size-limit ([#7760](https://github.com/chatmail/core/pull/7760)).
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Merge v2.38.0 into main branch.
|
||||
- Cleanup deprecated functions/defines ([#7763](https://github.com/chatmail/core/pull/7763)).
|
||||
|
||||
## [2.38.0] - 2026-01-22
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Jsonrpc: remove `contacts` from `FullChat`. To migrate load contacts on demand via `get_contacts_by_ids` using `FullChat.contactIds` ([#7282](https://github.com/chatmail/core/pull/7282)).
|
||||
- jsonrpc: Add run_until parameter for bots ([#7688](https://github.com/chatmail/core/pull/7688)).
|
||||
- rust, jsonrpc: Add `get_message_read_receipt_count` method ([#7732](https://github.com/chatmail/core/pull/7732)).
|
||||
- rust and jsonrpc: Marknoticed_all_chats method to mark all chats as noticed, including muted ones. ([#7709](https://github.com/chatmail/core/pull/7709)).
|
||||
- Public re-export of Connectivity ([#7737](https://github.com/chatmail/core/pull/7737)).
|
||||
|
||||
### Documentation
|
||||
|
||||
- Fix chat types.
|
||||
- Set_config_from_qr() configures context for "DCACCOUNT:" and "DCLOGIN:" QRs ([#7450](https://github.com/chatmail/core/pull/7450)).
|
||||
- Fix formatting of `indoc!` link.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Pre-messages / next version of download on demand ([#7371](https://github.com/chatmail/core/pull/7371)).
|
||||
- Connectivity view: move quota up and combine with IMAP state. ([#7653](https://github.com/chatmail/core/pull/7653)).
|
||||
- Execute sync message before checking for primary transport update.
|
||||
- Disable partial search by contact address.
|
||||
- Don't put text into post-message ([#7714](https://github.com/chatmail/core/pull/7714)).
|
||||
- Don't scale up Origin of multiple and broadcast recipients when sending a message.
|
||||
- pgp: Use preferred hash algorithm for signing instead of hardcoded SHA256.
|
||||
- In teamprofiles, don't mark chat as read on outgoing message ([#7717](https://github.com/chatmail/core/pull/7717)).
|
||||
- Send and apply MDNs to self ([#7005](https://github.com/chatmail/core/pull/7005))
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not show contact address in message info ([#7695](https://github.com/chatmail/core/pull/7695)).
|
||||
- Take transport_id into account when marking messages with \Seen flags.
|
||||
- Send bcc-self messages to all own relays ([#7656](https://github.com/chatmail/core/pull/7656)).
|
||||
- Only emit TransportsModified if transports are really modified.
|
||||
- Logging errors in deltachat-rpc-server during startup ([#7707](https://github.com/chatmail/core/pull/7707)).
|
||||
- Use only lowercase letters for stats id ([#7700](https://github.com/chatmail/core/pull/7700)).
|
||||
- Hide incoming broadcasts in `DC_GCL_FOR_FORWARDING` ([#7726](https://github.com/chatmail/core/pull/7726)).
|
||||
- Do not resolve ICE server hostnames during IMAP loop.
|
||||
- More reliable parsing of `dclogin:` links with ip address as host ([#7734](https://github.com/chatmail/core/pull/7734)).
|
||||
- Don't remember old channel members in the database ([#7716](https://github.com/chatmail/core/pull/7716)).
|
||||
- Make it possible to leave and immediately delete a chat ([#7744](https://github.com/chatmail/core/pull/7744)).
|
||||
- Emit MsgsChanged instead of MsgsNoticed on self-MDN if chat still has fresh messages.
|
||||
- Prevent possible infinite loop with invalid `smtp` row ([#7746](https://github.com/chatmail/core/pull/7746)).
|
||||
- Sync broadcast subscribers list ([#7578](https://github.com/chatmail/core/pull/7578))
|
||||
|
||||
### Refactor
|
||||
|
||||
- Don't use `concat!` in sql statements ([#7720](https://github.com/chatmail/core/pull/7720)).
|
||||
|
||||
### Tests
|
||||
|
||||
- Port test_dont_move_sync_msgs to JSON-RPC ([#7676](https://github.com/chatmail/core/pull/7676)).
|
||||
- rpc-client: Replace remaining print()s with `logging` ([#6082](https://github.com/chatmail/core/pull/6082)).
|
||||
|
||||
## [2.37.0] - 2026-01-08
|
||||
|
||||
### API-Changes
|
||||
|
||||
- JSON-RPC API `get_all_ui_config_keys` to get all "ui.*" config keys ([#7579](https://github.com/chatmail/core/pull/7579)).
|
||||
- Add `who_can_call_me` config option.
|
||||
- cffi api to create account manager with existing events channel to see events emitted during startup. `dc_event_channel_new`, `dc_event_channel_unref`, `dc_event_channel_get_event_emitter` and `dc_accounts_new_with_event_channel` ([#7609](https://github.com/chatmail/core/pull/7609)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Config option to skip seen synchronization ([#7694](https://github.com/chatmail/core/pull/7694)).
|
||||
- More text instead of sender in channel summary.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not rely on Secure-Join header to detect {vc,vg}-request.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update instructions to UI where to display the address.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: bump rsa from 0.9.9 to 0.9.10.
|
||||
- Update lru 0.12.3 to 0.12.5 and add RUSTSEC-2026-0002 exception.
|
||||
|
||||
### Refactor
|
||||
|
||||
- ffi: Replace implicit drop in cffi with explicit `drop(Arc::from_raw(var))` ([#7664](https://github.com/chatmail/core/pull/7664)).
|
||||
|
||||
### Tests
|
||||
|
||||
- Regression test for vc-request encrypted by the server.
|
||||
- Test that channel summary does not have sender name.
|
||||
|
||||
## [2.36.0] - 2026-01-03
|
||||
|
||||
### CI
|
||||
|
||||
- Pin GitHub Action references.
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add transports event to FFI.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add core version to `receive_imf` failure message.
|
||||
- Connectivity view: quota for all transports ([#7630](https://github.com/chatmail/core/pull/7630)).
|
||||
- Send sync messages over SMTP and do not move them to mvbox.
|
||||
|
||||
### Fixes
|
||||
|
||||
- When accepting group, add members with `Origin::IncomingTo` and sort them down in the contact list (7592).
|
||||
- Update fallback welcome message.
|
||||
- `inner_configure`: Check Config::OnlyFetchMvbox before MvboxMove for multi-transport ([#7637](https://github.com/chatmail/core/pull/7637)).
|
||||
- Reset options not available for chatmail on chatmail profiles.
|
||||
- Don't send webxdc notification for `notify: "*"` when chat is muted ([#7658](https://github.com/chatmail/core/pull/7658)).
|
||||
|
||||
### Documentation
|
||||
|
||||
- `delete_chat()`: don't lie that messages aren't deleted from server.
|
||||
- Remove references to removed `sentbox_watch` config.
|
||||
- Update documentation for `TransportsModified` event.
|
||||
|
||||
### Tests
|
||||
|
||||
- Contact list after accepting group with unknown contacts ([#7592](https://github.com/chatmail/core/pull/7592)).
|
||||
- Port test_import_export_online_all to JSON-RPC ([#7411](https://github.com/chatmail/core/pull/7411)).
|
||||
|
||||
### Refactor
|
||||
|
||||
- Turn `DC_VERSION_STR` into `&str`.
|
||||
- ffi: Remove one pointer indirection for `dc_accounts_t`.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- deps: Bump actions/download-artifact from 6 to 7.
|
||||
- deps: Bump actions/upload-artifact from 5 to 6.
|
||||
- deps: Bump astral-sh/setup-uv from 7.1.4 to 7.1.6.
|
||||
- deps: Bump cachix/install-nix-action from 31.8.4 to 31.9.0.
|
||||
- cargo: Bump serde_json from 1.0.145 to 1.0.147.
|
||||
- cargo: Bump uuid from 1.18.1 to 1.19.0.
|
||||
- cargo: Bump toml from 0.9.8 to 0.9.10+spec-1.1.0.
|
||||
- cargo: Bump tempfile from 3.23.0 to 3.24.0.
|
||||
- cargo: Bump libc from 0.2.177 to 0.2.178.
|
||||
- cargo: Bump tracing from 0.1.41 to 0.1.44.
|
||||
- cargo: Bump hyper-util from 0.1.18 to 0.1.19.
|
||||
- cargo: Bump log from 0.4.28 to 0.4.29.
|
||||
- cargo: Bump rustls-pki-types from 1.13.0 to 1.13.2.
|
||||
- cargo: Bump criterion from 0.7.0 to 0.8.1.
|
||||
|
||||
## [2.35.0] - 2025-12-16
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add blob dir size to storage info ([#7605](https://github.com/chatmail/core/pull/7605)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Use `turn.delta.chat` as fallback TURN server ([#7382](https://github.com/chatmail/core/pull/7382)).
|
||||
- Add ip addresses of known public chatmail relays from https://chatmail.at/relays to DNS cache ([#7607](https://github.com/chatmail/core/pull/7607)).
|
||||
- Improve error messages on adding relays.
|
||||
- Add transport addresses to IMAP URLs in message info.
|
||||
- `lookup_host_with_cache()`: Don't return empty address list ([#7596](https://github.com/chatmail/core/pull/7596)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- `get_chat_msgs_ex()`: Don't match on "S=" (Cmd) in param payload.
|
||||
- Remove `SecurejoinWait` info message when received Alice's key ([#7585](https://github.com/chatmail/core/pull/7585)).
|
||||
- Do not set normalized name for existing chats and contacts in a migration.
|
||||
- Remove now redundant "used_account_settings" and "entered_account_settings" from `Context.get_info()` ([#7587](https://github.com/chatmail/core/pull/7587)).
|
||||
- Don't use fallback servers if got TURN servers from IMAP METADATA.
|
||||
- Use fallback ICE servers if server can't IMAP METADATA ([#7382](https://github.com/chatmail/core/pull/7382)).
|
||||
- Add explicit limit for adding relays (5 at the moment) ([#7611](https://github.com/chatmail/core/pull/7611)).
|
||||
- Take `transport_id` into account when using `imap` table.
|
||||
|
||||
### CI
|
||||
|
||||
- Update Rust to 1.92.0.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Apply Rust 1.92.0 clippy suggestions.
|
||||
|
||||
### Other
|
||||
|
||||
- Log entered login params and actual used params on configuration failure ([#7610](https://github.com/chatmail/core/pull/7610)).
|
||||
|
||||
## [2.34.0] - 2025-12-11
|
||||
|
||||
### API-Changes
|
||||
|
||||
- rpc-client: Accept `Account` for `Chat.{add,remove}_contact()`.
|
||||
- rpc-client: Add `Chat.num_contacts()`.
|
||||
- Forwarding messages to another profile ([#7491](https://github.com/chatmail/core/pull/7491)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Double ringing time to 120 seconds.
|
||||
- Better logging for failing securejoin messages ([#7593](https://github.com/chatmail/core/pull/7593)).
|
||||
- Add multi-transport information to `Context.get_info` ([#7583](https://github.com/chatmail/core/pull/7583))
|
||||
|
||||
### Fixes
|
||||
|
||||
- Multi-transport: all transports were shown as "inbox" in connectivity view, now they are shown by their hostname ([#7582](https://github.com/chatmail/core/pull/7582)).
|
||||
- Multi-transport: Synchronize primary transport immediately after changing it.
|
||||
- Use u64 instead of usize to calculate storage usage.
|
||||
- Use u64 to represent the number of bytes in backup files.
|
||||
- Use u64 to count the number of bytes sent/received over the network.
|
||||
- Use logging macros instead of emitting event directly, so that it is also logged by tracing ([#7459](https://github.com/chatmail/core/pull/7459)).
|
||||
- Let securejoin succeed even if the chat was deleted in the meantime ([#7594](https://github.com/chatmail/core/pull/7594)).
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Add RUSTSEC-2025-0134 exception to deny.toml.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Use u16 instead of usize to represent progress bar.
|
||||
- Remove EncryptHelper.prefer_encrypt.
|
||||
- Add params when forwarding message instead of removing unneeded ones.
|
||||
|
||||
### Tests
|
||||
|
||||
- Port test_synchronize_member_list_on_group_rejoin to JSON-RPC.
|
||||
- Test setting up second device between core versions.
|
||||
|
||||
## [2.33.0] - 2025-12-05
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Case-insensitive search for non-ASCII chat and contact names ([#7477](https://github.com/chatmail/core/pull/7477)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Recognize all transport addresses as own addresses.
|
||||
|
||||
## [2.32.0] - 2025-12-04
|
||||
|
||||
Version bump to trigger publishing of npm prebuilds
|
||||
that failed to be published for 2.31.0 due to not configured "trusted publishers".
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- `lookup_or_create_adhoc_group()`: Add context to SQL errors ([#7554](https://github.com/chatmail/core/pull/7554)).
|
||||
|
||||
## [2.31.0] - 2025-12-04
|
||||
|
||||
### CI
|
||||
|
||||
- Update npm before publishing packages.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Use v2 SEIPD when sending messages to self.
|
||||
|
||||
## [2.30.0] - 2025-12-04
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Disable SNI for STARTTLS ([#7499](https://github.com/chatmail/core/pull/7499)).
|
||||
- Introduce cross-core testing along with improvements to test frameworking.
|
||||
- Synchronize transports via sync messages.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix shutdown shortly after call.
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add `TransportsModified` event (for tests).
|
||||
|
||||
### CI
|
||||
|
||||
- Use "trusted publishing" for NPM packages.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- deps: Bump actions/checkout from 5 to 6.
|
||||
- cargo: Bump syn from 2.0.110 to 2.0.111.
|
||||
- deps: Bump astral-sh/setup-uv from 7.1.3 to 7.1.4.
|
||||
- cargo: Bump sdp from 0.8.0 to 0.10.0.
|
||||
- Remove two outdated todo comments ([#7550](https://github.com/chatmail/core/pull/7550)).
|
||||
|
||||
## [2.29.0] - 2025-12-01
|
||||
|
||||
### API-Changes
|
||||
|
||||
- deltachat-rpc-client: Add Message.exists().
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- [**breaking**] Increase backup version from 3 to 4.
|
||||
- Hide `To` header in encrypted messages.
|
||||
- `deltachat_rpc_client.Rpc` accepts `rpc_server_path` for using a particular deltachat-rpc-server ([#7493](https://github.com/chatmail/core/pull/7493)).
|
||||
- Don't send `Chat-Group-Avatar` header in unencrypted groups.
|
||||
- Don't update `self-{avatar,status}` from received messages ([#7002](https://github.com/chatmail/core/pull/7002)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- `CREATE INDEX imap_only_rfc724_mid ON imap(rfc724_mid)` ([#7490](https://github.com/chatmail/core/pull/7490)).
|
||||
- Use the same webxdc ratelimit for all email servers.
|
||||
- Handle the case when account does not exist in `get_existing_msg_ids()`.
|
||||
- Don't send self-avatar in unencrypted messages ([#7136](https://github.com/chatmail/core/pull/7136)).
|
||||
- Do not configure folders during transport configuration.
|
||||
- Upload sync messages only with the primary transport.
|
||||
- Do not use deprecated ConfiguredProvider in get_configured_provider.
|
||||
|
||||
### Build system
|
||||
|
||||
- Make scripts for remote testing usable.
|
||||
- Increase minimum supported Python version to 3.10.
|
||||
- Use SPDX license expression in Python package metadata.
|
||||
|
||||
### CI
|
||||
|
||||
- Set timeout-minutes for all jobs in ci.yaml workflow.
|
||||
- Do not install Python manually to bulid RPC server wheels.
|
||||
- Do not build fake RPC server source packages.
|
||||
- Build Python wheels in separate jobs.
|
||||
|
||||
### Refactor
|
||||
|
||||
- [**breaking**] Remove some unneeded stock strings ([#7496](https://github.com/chatmail/core/pull/7496)).
|
||||
- Strike events in rpc-client request handling, get result from queue.
|
||||
- Use ConfiguredProvider config directly when loading legacy settings.
|
||||
- Remove update_icons and disable_server_delete migrations.
|
||||
- Use `SYMMETRIC_KEY_ALGORITHM` constant in `symm_encrypt_message()`.
|
||||
- Make signing key non-optional for `pk_encrypt`.
|
||||
|
||||
### Tests
|
||||
|
||||
- `test_remove_member_bcc`: Test unencrypted group as it was initially.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- deps: Bump cachix/install-nix-action from 31.8.1 to 31.8.4.
|
||||
- cargo: Bump hyper from 1.7.0 to 1.8.1.
|
||||
- cargo: Bump human-panic from 2.0.3 to 2.0.4.
|
||||
- cargo: Bump hyper-util from 0.1.17 to 0.1.18.
|
||||
- cargo: Bump rusqlite from 0.36.0 to 0.37.0.
|
||||
- cargo: Bump tokio-util from 0.7.16 to 0.7.17.
|
||||
- cargo: Bump toml from 0.9.7 to 0.9.8.
|
||||
- cargo: Bump proptest from 1.8.0 to 1.9.0.
|
||||
- cargo: Bump parking_lot from 0.12.4 to 0.12.5.
|
||||
- cargo: Bump syn from 2.0.106 to 2.0.110.
|
||||
- cargo: Bump quick-xml from 0.38.3 to 0.38.4.
|
||||
- cargo: Bump rustls-pki-types from 1.12.0 to 1.13.0.
|
||||
- cargo: Bump nu-ansi-term from 0.50.1 to 0.50.3.
|
||||
- cargo: Bump sanitize-filename from 0.5.0 to 0.6.0.
|
||||
- cargo: Bump quote from 1.0.41 to 1.0.42.
|
||||
- cargo: Bump libc from 0.2.176 to 0.2.177.
|
||||
- cargo: Bump bytes from 1.10.1 to 1.11.0.
|
||||
- cargo: Bump image from 0.25.8 to 0.25.9.
|
||||
- cargo: Bump rand from 0.9.0 to 0.9.2 ([#7501](https://github.com/chatmail/core/pull/7501)).
|
||||
- cargo: Bump tokio from 1.45.1 to 1.48.0.
|
||||
|
||||
## [2.28.0] - 2025-11-23
|
||||
|
||||
### API-Changes
|
||||
|
||||
- New API `get_existing_msg_ids()` to check if the messages with given IDs exist.
|
||||
- Add API to get storage usage information. (JSON-RPC method: `get_storage_usage_report_string`) ([#7486](https://github.com/chatmail/core/pull/7486)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Experimentaly allow adding second transport.
|
||||
There is no synchronization yet, so UIs should not allow the user to change the address manually and only expose the ability to add transports if `bcc_self` is disabled.
|
||||
- Default `bcc_self` to 0 for all new accounts.
|
||||
- Rephrase "Establishing end-to-end encryption" -> "Establishing connection".
|
||||
- Stock string for joining a channel ([#7480](https://github.com/chatmail/core/pull/7480)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Limit the range of `Date` to up to 6 days in the past.
|
||||
- `ContactId::set_name_ex()`: Emit ContactsChanged when transaction is completed.
|
||||
- Set SQLite busy timeout to 1 minute on iOS.
|
||||
- Sort system messages to the bottom of the chat.
|
||||
- Assign outgoing self-sent unencrypted messages to ad-hoc groups with only SELF ([#7409](https://github.com/chatmail/core/pull/7409)).
|
||||
- Add missing stock strings.
|
||||
- Look up or create ad-hoc group if there are duplicate addresses in "To".
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add missing RFC 9788, link 'Header Protection for Cryptographically Protected Email' as other RFC.
|
||||
- Remove unsupported RFC 3503 (`$MDNSent` flag) from the list of standards.
|
||||
- Mark database encryption support as deprecated ([#7403](https://github.com/chatmail/core/pull/7403)).
|
||||
|
||||
### Build system
|
||||
|
||||
- Increase Minimum Supported Rust Version to 1.88.0.
|
||||
- Update rPGP from 0.17.0 to 0.18.0.
|
||||
- nix: Update `fenix` and use it for all Rust builds.
|
||||
|
||||
### CI
|
||||
|
||||
- Do not use --encoding option for rst-lint.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Use `HashMap::extract_if()` stabilized in Rust 1.88.0.
|
||||
- Remove some easy to remove unwrap() calls.
|
||||
|
||||
### Tests
|
||||
|
||||
- Contact shalln't be verified by another having unknown verifier.
|
||||
|
||||
## [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
|
||||
@@ -7057,3 +7885,25 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[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
|
||||
[2.28.0]: https://github.com/chatmail/core/compare/v2.27.0..v2.28.0
|
||||
[2.29.0]: https://github.com/chatmail/core/compare/v2.28.0..v2.29.0
|
||||
[2.30.0]: https://github.com/chatmail/core/compare/v2.29.0..v2.30.0
|
||||
[2.31.0]: https://github.com/chatmail/core/compare/v2.30.0..v2.31.0
|
||||
[2.32.0]: https://github.com/chatmail/core/compare/v2.31.0..v2.32.0
|
||||
[2.33.0]: https://github.com/chatmail/core/compare/v2.32.0..v2.33.0
|
||||
[2.34.0]: https://github.com/chatmail/core/compare/v2.33.0..v2.34.0
|
||||
[2.35.0]: https://github.com/chatmail/core/compare/v2.34.0..v2.35.0
|
||||
[2.36.0]: https://github.com/chatmail/core/compare/v2.35.0..v2.36.0
|
||||
[2.37.0]: https://github.com/chatmail/core/compare/v2.36.0..v2.37.0
|
||||
[2.38.0]: https://github.com/chatmail/core/compare/v2.37.0..v2.38.0
|
||||
[2.39.0]: https://github.com/chatmail/core/compare/v2.38.0..v2.39.0
|
||||
[2.40.0]: https://github.com/chatmail/core/compare/v2.39.0..v2.40.0
|
||||
[2.41.0]: https://github.com/chatmail/core/compare/v2.40.0..v2.41.0
|
||||
[2.42.0]: https://github.com/chatmail/core/compare/v2.41.0..v2.42.0
|
||||
[2.43.0]: https://github.com/chatmail/core/compare/v2.42.0..v2.43.0
|
||||
[2.44.0]: https://github.com/chatmail/core/compare/v2.43.0..v2.44.0
|
||||
[2.45.0]: https://github.com/chatmail/core/compare/v2.44.0..v2.45.0
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Contributing to Delta Chat
|
||||
# Contributing to chatmail core
|
||||
|
||||
## Bug reports
|
||||
|
||||
|
||||
790
Cargo.lock
generated
790
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
40
Cargo.toml
40
Cargo.toml
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "2.23.0"
|
||||
version = "2.46.0-dev"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.85"
|
||||
rust-version = "1.88"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
[profile.dev]
|
||||
@@ -45,7 +45,7 @@ anyhow = { workspace = true }
|
||||
async-broadcast = "0.7.2"
|
||||
async-channel = { workspace = true }
|
||||
async-imap = { version = "0.11.1", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
|
||||
async-native-tls = { version = "0.6", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
|
||||
async_zip = { version = "0.0.18", default-features = false, features = ["deflate", "tokio-fs"] }
|
||||
base64 = { workspace = true }
|
||||
@@ -56,7 +56,7 @@ chrono = { workspace = true, features = ["alloc", "clock", "std"] }
|
||||
colorutils-rs = { version = "0.7.5", default-features = false }
|
||||
data-encoding = "2.9.0"
|
||||
escaper = "0.1"
|
||||
fast-socks5 = "0.10"
|
||||
fast-socks5 = "1"
|
||||
fd-lock = "4"
|
||||
futures-lite = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
@@ -78,17 +78,17 @@ num-derive = "0.4"
|
||||
num-traits = { workspace = true }
|
||||
parking_lot = "0.12.4"
|
||||
percent-encoding = "2.3"
|
||||
pgp = { version = "0.17.0", default-features = false }
|
||||
pgp = { version = "0.19.0", default-features = false }
|
||||
pin-project = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = { version = "0.38", features = ["escape-html"] }
|
||||
quick-xml = { version = "0.39", features = ["escape-html"] }
|
||||
rand-old = { package = "rand", version = "0.8" }
|
||||
rand = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||
rustls-pki-types = "1.12.0"
|
||||
sanitize-filename = { workspace = true }
|
||||
sdp = "0.8.0"
|
||||
sdp = "0.10.0"
|
||||
serde_json = { workspace = true }
|
||||
serde_urlencoded = "0.7.1"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
@@ -96,8 +96,8 @@ sha-1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
shadowsocks = { version = "1.23.1", default-features = false, features = ["aead-cipher", "aead-cipher-2022"] }
|
||||
smallvec = "1.15.1"
|
||||
strum = "0.27"
|
||||
strum_macros = "0.27"
|
||||
strum = "0.28"
|
||||
strum_macros = "0.28"
|
||||
tagger = "4.3.4"
|
||||
textwrap = "0.16.2"
|
||||
thiserror = { workspace = true }
|
||||
@@ -111,11 +111,12 @@ toml = "0.9"
|
||||
tracing = "0.1.41"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
walkdir = "2.5.0"
|
||||
webpki-roots = "0.26.8"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
criterion = { version = "0.7.0", features = ["async_tokio"] }
|
||||
criterion = { version = "0.8.1", features = ["async_tokio"] }
|
||||
futures-lite = { workspace = true }
|
||||
log = { workspace = true }
|
||||
nu-ansi-term = { workspace = true }
|
||||
@@ -156,6 +157,11 @@ name = "receive_emails"
|
||||
required-features = ["internals"]
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "decrypting"
|
||||
required-features = ["internals"]
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "get_chat_msgs"
|
||||
harness = false
|
||||
@@ -176,11 +182,11 @@ harness = false
|
||||
anyhow = "1"
|
||||
async-channel = "2.5.0"
|
||||
base64 = "0.22"
|
||||
chrono = { version = "0.4.42", default-features = false }
|
||||
chrono = { version = "0.4.43", default-features = false }
|
||||
deltachat-contact-tools = { path = "deltachat-contact-tools" }
|
||||
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
|
||||
deltachat = { path = ".", default-features = false }
|
||||
futures = "0.3.31"
|
||||
futures = "0.3.32"
|
||||
futures-lite = "2.6.1"
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
@@ -188,15 +194,15 @@ mailparse = "0.16.1"
|
||||
nu-ansi-term = "0.50"
|
||||
num-traits = "0.2"
|
||||
rand = "0.9"
|
||||
regex = "1.10"
|
||||
rusqlite = "0.36"
|
||||
sanitize-filename = "0.5"
|
||||
regex = "1.12"
|
||||
rusqlite = "0.37"
|
||||
sanitize-filename = "0.6"
|
||||
serde = "1.0"
|
||||
serde_json = "1"
|
||||
tempfile = "3.23.0"
|
||||
tempfile = "3.25.0"
|
||||
thiserror = "2"
|
||||
tokio = "1"
|
||||
tokio-util = "0.7.16"
|
||||
tokio-util = "0.7.18"
|
||||
tracing-subscriber = "0.3"
|
||||
yerpc = "0.6.4"
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
55
RELEASE.md
55
RELEASE.md
@@ -1,4 +1,4 @@
|
||||
# Releasing a new version of DeltaChat core
|
||||
# Releasing a new version of chatmail core
|
||||
|
||||
For example, to release version 1.116.0 of the core, do the following steps.
|
||||
|
||||
@@ -14,8 +14,55 @@ For example, to release version 1.116.0 of the core, do the following steps.
|
||||
5. Commit the changes as `chore(release): prepare for 1.116.0`.
|
||||
Optionally, use a separate branch like `prep-1.116.0` for this commit and open a PR for review.
|
||||
|
||||
6. Tag the release: `git tag --annotate v1.116.0`.
|
||||
6. Push the commit to the `main` branch.
|
||||
|
||||
7. Push the release tag: `git push origin v1.116.0`.
|
||||
7. Once the commit is on the `main` branch and passed CI, tag the release: `git tag --annotate v1.116.0`.
|
||||
|
||||
8. Create a GitHub release: `gh release create v1.116.0 --notes ''`.
|
||||
8. Push the release tag: `git push origin v1.116.0`.
|
||||
|
||||
9. Create a GitHub release: `gh release create v1.116.0 --notes ''`.
|
||||
|
||||
10. Update the version to the next development version:
|
||||
`scripts/set_core_version.py 1.117.0-dev`.
|
||||
|
||||
11. Commit and push the change:
|
||||
`git commit -m "chore: bump version to 1.117.0-dev" && git push origin main`.
|
||||
|
||||
12. Once the binaries are generated and [published](https://github.com/chatmail/core/releases),
|
||||
check Windows binaries for false positive detections at [VirusTotal].
|
||||
Either upload the binaries directly or submit a direct link to the artifact.
|
||||
You can use [old browsers interface](https://www.virustotal.com/old-browsers/)
|
||||
if there are problems with using the default website.
|
||||
If you submit a direct link and get to the page saying
|
||||
"No security vendors flagged this URL as malicious",
|
||||
it does not mean that the file itself is not detected.
|
||||
You need to go to the "details" tab
|
||||
and click on the SHA-256 hash in the "Body SHA-256" section.
|
||||
If any false positive is detected,
|
||||
open an issue to track removing it.
|
||||
See <https://github.com/chatmail/core/issues/7847>
|
||||
for an example of false positive detection issue.
|
||||
If there is a false positive "Microsoft" detection,
|
||||
mark the issue as a blocker.
|
||||
|
||||
[VirusTotal]: https://www.virustotal.com/
|
||||
|
||||
## Dealing with antivirus false positives
|
||||
|
||||
If Windows release is incorrectly detected by some antivirus, submit requests to remove detection.
|
||||
|
||||
"Microsoft" antivirus is built in Windows and will break user setups so removing its detection should be highest priority.
|
||||
To submit false positive to Microsoft, go to <https://www.microsoft.com/en-us/wdsi/filesubmission> and select "Submit file as a ... Software developer" option.
|
||||
|
||||
False positive contacts for other vendors can be found at <https://docs.virustotal.com/docs/false-positive-contacts>.
|
||||
Not all of them may be up to date, so check the links below first.
|
||||
Previously we successfully used the following contacts:
|
||||
- [ESET-NOD32](mailto:samples@eset.com)
|
||||
- [Symantec](https://symsubmit.symantec.com/)
|
||||
|
||||
## Dealing with failed releases
|
||||
|
||||
Once you make a GitHub release,
|
||||
CI will try to build and publish [PyPI](https://pypi.org/) and [npm](https://www.npmjs.com/) packages.
|
||||
If this fails for some reason, do not modify the failed tag, do not delete it and do not force-push to the `main` branch.
|
||||
Fix the build process and tag a new release instead.
|
||||
|
||||
11
STYLE.md
11
STYLE.md
@@ -16,11 +16,12 @@ id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
text TEXT DEFAULT '' NOT NULL -- message text
|
||||
) STRICT",
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.context("CREATE TABLE messages")?;
|
||||
```
|
||||
|
||||
Do not use macros like [`concat!`](https://doc.rust-lang.org/std/macro.concat.html)
|
||||
or [`indoc!](https://docs.rs/indoc).
|
||||
or [`indoc!`](https://docs.rs/indoc).
|
||||
Do not escape newlines like this:
|
||||
```
|
||||
sql.execute(
|
||||
@@ -29,7 +30,8 @@ id INTEGER PRIMARY KEY AUTOINCREMENT, \
|
||||
text TEXT DEFAULT '' NOT NULL \
|
||||
) STRICT",
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.context("CREATE TABLE messages")?;
|
||||
```
|
||||
Escaping newlines
|
||||
is prone to errors like this if space before backslash is missing:
|
||||
@@ -63,6 +65,9 @@ an older version. Also don't change the column type, consider adding a new colum
|
||||
instead. Finally, never change column semantics, this is especially dangerous because the `STRICT`
|
||||
keyword doesn't help here.
|
||||
|
||||
Consider adding context to `anyhow` errors for SQL statements using `.context()` so that it's
|
||||
possible to understand from logs which statement failed. See [Errors](#errors) for more info.
|
||||
|
||||
## Errors
|
||||
|
||||
Delta Chat core mostly uses [`anyhow`](https://docs.rs/anyhow/) errors.
|
||||
|
||||
142
benches/decrypting.rs
Normal file
142
benches/decrypting.rs
Normal file
@@ -0,0 +1,142 @@
|
||||
//! 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 and parse a symmetrically encrypted message' benchmark:
|
||||
//!
|
||||
//! ```text
|
||||
//! cargo bench --bench decrypting --features="internals" -- 'Decrypt and parse a symmetrically encrypted message'
|
||||
//! ```
|
||||
//!
|
||||
//! You can also pass a substring:
|
||||
//!
|
||||
//! ```text
|
||||
//! cargo bench --bench decrypting --features="internals" -- 'symmetrically'
|
||||
//! ```
|
||||
//!
|
||||
//! 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 std::sync::LazyLock;
|
||||
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
use deltachat::internals_for_benches::create_broadcast_secret;
|
||||
use deltachat::internals_for_benches::save_broadcast_secret;
|
||||
use deltachat::securejoin::get_securejoin_qr;
|
||||
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,
|
||||
stock_str::StockStrings,
|
||||
};
|
||||
use tempfile::tempdir;
|
||||
|
||||
static NUM_BROADCAST_SECRETS: LazyLock<usize> = LazyLock::new(|| {
|
||||
std::env::var("NUM_BROADCAST_SECRETS")
|
||||
.unwrap_or("500".to_string())
|
||||
.parse()
|
||||
.unwrap()
|
||||
});
|
||||
static NUM_AUTH_TOKENS: LazyLock<usize> = LazyLock::new(|| {
|
||||
std::env::var("NUM_AUTH_TOKENS")
|
||||
.unwrap_or("5000".to_string())
|
||||
.parse()
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
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();
|
||||
store_self_keypair(&context, &secret)
|
||||
.await
|
||||
.expect("Failed to save key");
|
||||
|
||||
context
|
||||
}
|
||||
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("Decrypt");
|
||||
|
||||
// ===========================================================================================
|
||||
// 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_BROADCAST_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();
|
||||
}
|
||||
for _i in 0..*NUM_AUTH_TOKENS {
|
||||
get_securejoin_qr(&context, None).await.unwrap();
|
||||
}
|
||||
println!("NUM_AUTH_TOKENS={}", *NUM_AUTH_TOKENS);
|
||||
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!(black_box(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!(black_box(text), "hi");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn generate_secrets() -> Vec<String> {
|
||||
let secrets: Vec<String> = (0..*NUM_BROADCAST_SECRETS)
|
||||
.map(|_| create_broadcast_secret())
|
||||
.collect();
|
||||
println!("NUM_BROADCAST_SECRETS={}", *NUM_BROADCAST_SECRETS);
|
||||
secrets
|
||||
}
|
||||
|
||||
criterion_group!(benches, criterion_benchmark);
|
||||
criterion_main!(benches);
|
||||
@@ -66,7 +66,7 @@ body = """
|
||||
{% for commit in commits %}
|
||||
- {% if commit.breaking %}[**breaking**] {% endif %}\
|
||||
{% if commit.scope %}{{ commit.scope }}: {% endif %}\
|
||||
{{ commit.message | upper_first }}.\
|
||||
{{ commit.message }}.\
|
||||
{% if commit.footers is defined %}\
|
||||
{% for footer in commit.footers %}{% if 'BREAKING CHANGE' in footer.token %}
|
||||
{% raw %} {% endraw %}- {{ footer.value }}\
|
||||
|
||||
@@ -36,6 +36,45 @@ impl VcardContact {
|
||||
}
|
||||
}
|
||||
|
||||
fn escape(s: &str) -> String {
|
||||
// https://www.rfc-editor.org/rfc/rfc6350.html#section-3.4
|
||||
s
|
||||
// backslash must be first!
|
||||
.replace(r"\", r"\\")
|
||||
.replace(',', r"\,")
|
||||
.replace(';', r"\;")
|
||||
.replace('\n', r"\n")
|
||||
}
|
||||
|
||||
fn unescape(s: &str) -> String {
|
||||
// https://www.rfc-editor.org/rfc/rfc6350.html#section-3.4
|
||||
let mut out = String::new();
|
||||
|
||||
let mut chars = s.chars();
|
||||
while let Some(c) = chars.next() {
|
||||
if c == '\\' {
|
||||
if let Some(next) = chars.next() {
|
||||
match next {
|
||||
'\\' | ',' | ';' => out.push(next),
|
||||
'n' | 'N' => out.push('\n'),
|
||||
_ => {
|
||||
// Invalid escape sequence (keep unchanged)
|
||||
out.push('\\');
|
||||
out.push(next);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Invalid escape sequence (keep unchanged)
|
||||
out.push('\\');
|
||||
}
|
||||
} else {
|
||||
out.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
/// Returns a vCard containing given contacts.
|
||||
///
|
||||
/// Calling [`parse_vcard()`] on the returned result is a reverse operation.
|
||||
@@ -46,10 +85,6 @@ pub fn make_vcard(contacts: &[VcardContact]) -> String {
|
||||
Some(datetime.format("%Y%m%dT%H%M%SZ").to_string())
|
||||
}
|
||||
|
||||
fn escape(s: &str) -> String {
|
||||
s.replace(',', "\\,")
|
||||
}
|
||||
|
||||
let mut res = "".to_string();
|
||||
for c in contacts {
|
||||
// Mustn't contain ',', but it's easier to escape than to error out.
|
||||
@@ -124,7 +159,7 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||
fn vcard_property<'a>(line: &'a str, property: &str) -> Option<(&'a str, String)> {
|
||||
let (params, value) = vcard_property_raw(line, property)?;
|
||||
// Some fields can't contain commas, but unescape them everywhere for safety.
|
||||
Some((params, value.replace("\\,", ",")))
|
||||
Some((params, unescape(value)))
|
||||
}
|
||||
fn base64_key(line: &str) -> Option<&str> {
|
||||
let (params, value) = vcard_property_raw(line, "key")?;
|
||||
|
||||
@@ -91,7 +91,7 @@ fn test_make_and_parse_vcard() {
|
||||
authname: "Alice Wonderland".to_string(),
|
||||
key: Some("[base64-data]".to_string()),
|
||||
profile_image: Some("image in Base64".to_string()),
|
||||
biography: Some("Hi, I'm Alice".to_string()),
|
||||
biography: Some("Hi,\nI'm Alice; and this is a backslash: \\".to_string()),
|
||||
timestamp: Ok(1713465762),
|
||||
},
|
||||
VcardContact {
|
||||
@@ -110,7 +110,7 @@ fn test_make_and_parse_vcard() {
|
||||
FN:Alice Wonderland\r\n\
|
||||
KEY:data:application/pgp-keys;base64\\,[base64-data]\r\n\
|
||||
PHOTO:data:image/jpeg;base64\\,image in Base64\r\n\
|
||||
NOTE:Hi\\, I'm Alice\r\n\
|
||||
NOTE:Hi\\,\\nI'm Alice\\; and this is a backslash: \\\\\r\n\
|
||||
REV:20240418T184242Z\r\n\
|
||||
END:VCARD\r\n",
|
||||
"BEGIN:VCARD\r\n\
|
||||
@@ -276,3 +276,14 @@ END:VCARD",
|
||||
assert!(contacts[0].timestamp.is_err());
|
||||
assert_eq!(contacts[0].profile_image.as_ref().unwrap(), "/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vcard_value_escape_unescape() {
|
||||
let original = "Text, with; chars and a \\ and a newline\nand a literal newline \\n";
|
||||
let expected_escaped = r"Text\, with\; chars and a \\ and a newline\nand a literal newline \\n";
|
||||
|
||||
let escaped = escape(original);
|
||||
assert_eq!(escaped, expected_escaped);
|
||||
let unescaped = unescape(&escaped);
|
||||
assert_eq!(original, unescaped);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.23.0"
|
||||
version = "2.46.0-dev"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,10 +15,10 @@ use std::collections::BTreeMap;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt::Write;
|
||||
use std::future::Future;
|
||||
use std::ops::Deref;
|
||||
use std::mem::ManuallyDrop;
|
||||
use std::ptr;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use std::sync::{Arc, LazyLock, Mutex};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::Context as _;
|
||||
@@ -559,6 +559,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::IncomingCallAccepted { .. } => 2560,
|
||||
EventType::OutgoingCallAccepted { .. } => 2570,
|
||||
EventType::CallEnded { .. } => 2580,
|
||||
EventType::TransportsModified => 2600,
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||
@@ -593,7 +594,8 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::AccountsBackgroundFetchDone
|
||||
| EventType::ChatlistChanged
|
||||
| EventType::AccountsChanged
|
||||
| EventType::AccountsItemChanged => 0,
|
||||
| EventType::AccountsItemChanged
|
||||
| EventType::TransportsModified => 0,
|
||||
EventType::IncomingReaction { contact_id, .. }
|
||||
| EventType::IncomingWebxdcNotify { contact_id, .. } => contact_id.to_u32() as libc::c_int,
|
||||
EventType::MsgsChanged { chat_id, .. }
|
||||
@@ -678,10 +680,10 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::ChatModified(_)
|
||||
| EventType::ChatDeleted { .. }
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
|
||||
| EventType::IncomingCallAccepted { .. }
|
||||
| EventType::OutgoingCallAccepted { .. }
|
||||
| EventType::CallEnded { .. }
|
||||
| EventType::EventChannelOverflow { .. } => 0,
|
||||
| EventType::EventChannelOverflow { .. }
|
||||
| EventType::TransportsModified => 0,
|
||||
EventType::MsgsChanged { msg_id, .. }
|
||||
| EventType::ReactionsChanged { msg_id, .. }
|
||||
| EventType::IncomingReaction { msg_id, .. }
|
||||
@@ -700,6 +702,9 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
} => status_update_serial.to_u32() as libc::c_int,
|
||||
EventType::WebxdcRealtimeData { data, .. } => data.len() as libc::c_int,
|
||||
EventType::IncomingCall { has_video, .. } => *has_video as libc::c_int,
|
||||
EventType::IncomingCallAccepted {
|
||||
from_this_device, ..
|
||||
} => *from_this_device as libc::c_int,
|
||||
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
@@ -780,7 +785,8 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::AccountsChanged
|
||||
| EventType::AccountsItemChanged
|
||||
| EventType::IncomingCallAccepted { .. }
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. } => ptr::null_mut(),
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
|
||||
| EventType::TransportsModified => ptr::null_mut(),
|
||||
EventType::IncomingCall {
|
||||
place_call_info, ..
|
||||
} => {
|
||||
@@ -1178,6 +1184,7 @@ pub unsafe extern "C" fn dc_place_outgoing_call(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
place_call_info: *const libc::c_char,
|
||||
has_video: bool,
|
||||
) -> u32 {
|
||||
if context.is_null() || chat_id == 0 {
|
||||
eprintln!("ignoring careless call to dc_place_outgoing_call()");
|
||||
@@ -1187,7 +1194,7 @@ pub unsafe extern "C" fn dc_place_outgoing_call(
|
||||
let chat_id = ChatId::new(chat_id);
|
||||
let place_call_info = to_string_lossy(place_call_info);
|
||||
|
||||
block_on(ctx.place_outgoing_call(chat_id, place_call_info))
|
||||
block_on(ctx.place_outgoing_call(chat_id, place_call_info, has_video))
|
||||
.context("Failed to place call")
|
||||
.log_err(ctx)
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
@@ -2257,22 +2264,6 @@ pub unsafe extern "C" fn dc_get_contacts(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_blocked_cnt(context: *mut dc_context_t) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_blocked_cnt()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
Contact::get_all_blocked(ctx)
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed to get blocked count")
|
||||
.len() as libc::c_int
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_blocked_contacts(
|
||||
context: *mut dc_context_t,
|
||||
@@ -4240,7 +4231,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]
|
||||
@@ -4725,33 +4726,13 @@ pub unsafe extern "C" fn dc_provider_unref(provider: *mut dc_provider_t) {
|
||||
|
||||
/// Reader-writer lock wrapper for accounts manager to guarantee thread safety when using
|
||||
/// `dc_accounts_t` in multiple threads at once.
|
||||
pub struct AccountsWrapper {
|
||||
inner: Arc<RwLock<Accounts>>,
|
||||
}
|
||||
|
||||
impl Deref for AccountsWrapper {
|
||||
type Target = Arc<RwLock<Accounts>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl AccountsWrapper {
|
||||
fn new(accounts: Accounts) -> Self {
|
||||
let inner = Arc::new(RwLock::new(accounts));
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct representing a list of deltachat accounts.
|
||||
pub type dc_accounts_t = AccountsWrapper;
|
||||
pub type dc_accounts_t = RwLock<Accounts>;
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_new(
|
||||
dir: *const libc::c_char,
|
||||
writable: libc::c_int,
|
||||
) -> *mut dc_accounts_t {
|
||||
) -> *const dc_accounts_t {
|
||||
setup_panic!();
|
||||
|
||||
if dir.is_null() {
|
||||
@@ -4762,7 +4743,99 @@ pub unsafe extern "C" fn dc_accounts_new(
|
||||
let accs = block_on(Accounts::new(as_path(dir).into(), writable != 0));
|
||||
|
||||
match accs {
|
||||
Ok(accs) => Box::into_raw(Box::new(AccountsWrapper::new(accs))),
|
||||
Ok(accs) => Arc::into_raw(Arc::new(RwLock::new(accs))),
|
||||
Err(err) => {
|
||||
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
|
||||
eprintln!("failed to create accounts: {err:#}");
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type dc_event_channel_t = Mutex<Option<Events>>;
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_event_channel_new() -> *mut dc_event_channel_t {
|
||||
Box::into_raw(Box::new(Mutex::new(Some(Events::new()))))
|
||||
}
|
||||
|
||||
/// Release the events channel structure.
|
||||
///
|
||||
/// This function releases the memory of the `dc_event_channel_t` structure.
|
||||
///
|
||||
/// you can call it after calling dc_accounts_new_with_event_channel,
|
||||
/// which took the events channel out of it already, so this just frees the underlying option.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_event_channel_unref(event_channel: *mut dc_event_channel_t) {
|
||||
if event_channel.is_null() {
|
||||
eprintln!("ignoring careless call to dc_event_channel_unref()");
|
||||
return;
|
||||
}
|
||||
drop(Box::from_raw(event_channel))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_event_channel_get_event_emitter(
|
||||
event_channel: *mut dc_event_channel_t,
|
||||
) -> *mut dc_event_emitter_t {
|
||||
if event_channel.is_null() {
|
||||
eprintln!("ignoring careless call to dc_event_channel_get_event_emitter()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let Some(event_channel) = &*(*event_channel)
|
||||
.lock()
|
||||
.expect("call to dc_event_channel_get_event_emitter() failed: mutex is poisoned")
|
||||
else {
|
||||
eprintln!(
|
||||
"ignoring careless call to dc_event_channel_get_event_emitter()
|
||||
-> channel was already consumed, make sure you call this before dc_accounts_new_with_event_channel"
|
||||
);
|
||||
return ptr::null_mut();
|
||||
};
|
||||
|
||||
let emitter = event_channel.get_emitter();
|
||||
|
||||
Box::into_raw(Box::new(emitter))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_new_with_event_channel(
|
||||
dir: *const libc::c_char,
|
||||
writable: libc::c_int,
|
||||
event_channel: *mut dc_event_channel_t,
|
||||
) -> *const dc_accounts_t {
|
||||
setup_panic!();
|
||||
|
||||
if dir.is_null() || event_channel.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_new_with_event_channel()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
// consuming channel enforce that you need to get the event emitter
|
||||
// before initializing the account manager,
|
||||
// so that you don't miss events/errors during initialisation.
|
||||
// It also prevents you from using the same channel on multiple account managers.
|
||||
let Some(event_channel) = (*event_channel)
|
||||
.lock()
|
||||
.expect("call to dc_event_channel_get_event_emitter() failed: mutex is poisoned")
|
||||
.take()
|
||||
else {
|
||||
eprintln!(
|
||||
"ignoring careless call to dc_accounts_new_with_event_channel()
|
||||
-> channel was already consumed"
|
||||
);
|
||||
return ptr::null_mut();
|
||||
};
|
||||
|
||||
let accs = block_on(Accounts::new_with_events(
|
||||
as_path(dir).into(),
|
||||
writable != 0,
|
||||
event_channel,
|
||||
));
|
||||
|
||||
match accs {
|
||||
Ok(accs) => Arc::into_raw(Arc::new(RwLock::new(accs))),
|
||||
Err(err) => {
|
||||
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
|
||||
eprintln!("failed to create accounts: {err:#}");
|
||||
@@ -4775,17 +4848,17 @@ pub unsafe extern "C" fn dc_accounts_new(
|
||||
///
|
||||
/// This function releases the memory of the `dc_accounts_t` structure.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_unref(accounts: *mut dc_accounts_t) {
|
||||
pub unsafe extern "C" fn dc_accounts_unref(accounts: *const dc_accounts_t) {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_unref()");
|
||||
return;
|
||||
}
|
||||
let _ = Box::from_raw(accounts);
|
||||
drop(Arc::from_raw(accounts));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_get_account(
|
||||
accounts: *mut dc_accounts_t,
|
||||
accounts: *const dc_accounts_t,
|
||||
id: u32,
|
||||
) -> *mut dc_context_t {
|
||||
if accounts.is_null() {
|
||||
@@ -4802,7 +4875,7 @@ pub unsafe extern "C" fn dc_accounts_get_account(
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_get_selected_account(
|
||||
accounts: *mut dc_accounts_t,
|
||||
accounts: *const dc_accounts_t,
|
||||
) -> *mut dc_context_t {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_get_selected_account()");
|
||||
@@ -4818,7 +4891,7 @@ pub unsafe extern "C" fn dc_accounts_get_selected_account(
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_select_account(
|
||||
accounts: *mut dc_accounts_t,
|
||||
accounts: *const dc_accounts_t,
|
||||
id: u32,
|
||||
) -> libc::c_int {
|
||||
if accounts.is_null() {
|
||||
@@ -4842,13 +4915,13 @@ pub unsafe extern "C" fn dc_accounts_select_account(
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_add_account(accounts: *mut dc_accounts_t) -> u32 {
|
||||
pub unsafe extern "C" fn dc_accounts_add_account(accounts: *const dc_accounts_t) -> u32 {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_add_account()");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let accounts = &mut *accounts;
|
||||
let accounts = &*accounts;
|
||||
|
||||
block_on(async move {
|
||||
let mut accounts = accounts.write().await;
|
||||
@@ -4863,13 +4936,13 @@ pub unsafe extern "C" fn dc_accounts_add_account(accounts: *mut dc_accounts_t) -
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_add_closed_account(accounts: *mut dc_accounts_t) -> u32 {
|
||||
pub unsafe extern "C" fn dc_accounts_add_closed_account(accounts: *const dc_accounts_t) -> u32 {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_add_closed_account()");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let accounts = &mut *accounts;
|
||||
let accounts = &*accounts;
|
||||
|
||||
block_on(async move {
|
||||
let mut accounts = accounts.write().await;
|
||||
@@ -4885,7 +4958,7 @@ pub unsafe extern "C" fn dc_accounts_add_closed_account(accounts: *mut dc_accoun
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_remove_account(
|
||||
accounts: *mut dc_accounts_t,
|
||||
accounts: *const dc_accounts_t,
|
||||
id: u32,
|
||||
) -> libc::c_int {
|
||||
if accounts.is_null() {
|
||||
@@ -4893,7 +4966,7 @@ pub unsafe extern "C" fn dc_accounts_remove_account(
|
||||
return 0;
|
||||
}
|
||||
|
||||
let accounts = &mut *accounts;
|
||||
let accounts = &*accounts;
|
||||
|
||||
block_on(async move {
|
||||
let mut accounts = accounts.write().await;
|
||||
@@ -4911,7 +4984,7 @@ pub unsafe extern "C" fn dc_accounts_remove_account(
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_migrate_account(
|
||||
accounts: *mut dc_accounts_t,
|
||||
accounts: *const dc_accounts_t,
|
||||
dbfile: *const libc::c_char,
|
||||
) -> u32 {
|
||||
if accounts.is_null() || dbfile.is_null() {
|
||||
@@ -4919,7 +4992,7 @@ pub unsafe extern "C" fn dc_accounts_migrate_account(
|
||||
return 0;
|
||||
}
|
||||
|
||||
let accounts = &mut *accounts;
|
||||
let accounts = &*accounts;
|
||||
let dbfile = to_string_lossy(dbfile);
|
||||
|
||||
block_on(async move {
|
||||
@@ -4940,7 +5013,7 @@ pub unsafe extern "C" fn dc_accounts_migrate_account(
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_get_all(accounts: *mut dc_accounts_t) -> *mut dc_array_t {
|
||||
pub unsafe extern "C" fn dc_accounts_get_all(accounts: *const dc_accounts_t) -> *mut dc_array_t {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_get_all()");
|
||||
return ptr::null_mut();
|
||||
@@ -4954,18 +5027,18 @@ pub unsafe extern "C" fn dc_accounts_get_all(accounts: *mut dc_accounts_t) -> *m
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_start_io(accounts: *mut dc_accounts_t) {
|
||||
pub unsafe extern "C" fn dc_accounts_start_io(accounts: *const dc_accounts_t) {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_start_io()");
|
||||
return;
|
||||
}
|
||||
|
||||
let accounts = &mut *accounts;
|
||||
let accounts = &*accounts;
|
||||
block_on(async move { accounts.write().await.start_io().await });
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_stop_io(accounts: *mut dc_accounts_t) {
|
||||
pub unsafe extern "C" fn dc_accounts_stop_io(accounts: *const dc_accounts_t) {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_stop_io()");
|
||||
return;
|
||||
@@ -4976,7 +5049,7 @@ pub unsafe extern "C" fn dc_accounts_stop_io(accounts: *mut dc_accounts_t) {
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_maybe_network(accounts: *mut dc_accounts_t) {
|
||||
pub unsafe extern "C" fn dc_accounts_maybe_network(accounts: *const dc_accounts_t) {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_maybe_network()");
|
||||
return;
|
||||
@@ -4987,7 +5060,7 @@ pub unsafe extern "C" fn dc_accounts_maybe_network(accounts: *mut dc_accounts_t)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *mut dc_accounts_t) {
|
||||
pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *const dc_accounts_t) {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_maybe_network_lost()");
|
||||
return;
|
||||
@@ -4999,7 +5072,7 @@ pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *mut dc_accoun
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_background_fetch(
|
||||
accounts: *mut dc_accounts_t,
|
||||
accounts: *const dc_accounts_t,
|
||||
timeout_in_seconds: u64,
|
||||
) -> libc::c_int {
|
||||
if accounts.is_null() || timeout_in_seconds <= 2 {
|
||||
@@ -5017,9 +5090,20 @@ pub unsafe extern "C" fn dc_accounts_background_fetch(
|
||||
1
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_stop_background_fetch(accounts: *const dc_accounts_t) {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_stop_background_fetch()");
|
||||
return;
|
||||
}
|
||||
|
||||
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,
|
||||
accounts: *const dc_accounts_t,
|
||||
token: *const libc::c_char,
|
||||
) {
|
||||
if accounts.is_null() {
|
||||
@@ -5042,7 +5126,7 @@ pub unsafe extern "C" fn dc_accounts_set_push_device_token(
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_get_event_emitter(
|
||||
accounts: *mut dc_accounts_t,
|
||||
accounts: *const dc_accounts_t,
|
||||
) -> *mut dc_event_emitter_t {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_get_event_emitter()");
|
||||
@@ -5062,17 +5146,17 @@ pub struct dc_jsonrpc_instance_t {
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_jsonrpc_init(
|
||||
account_manager: *mut dc_accounts_t,
|
||||
account_manager: *const dc_accounts_t,
|
||||
) -> *mut dc_jsonrpc_instance_t {
|
||||
if account_manager.is_null() {
|
||||
eprintln!("ignoring careless call to dc_jsonrpc_init()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let account_manager = &*account_manager;
|
||||
let cmd_api = block_on(deltachat_jsonrpc::api::CommandApi::from_arc(
|
||||
account_manager.inner.clone(),
|
||||
));
|
||||
let account_manager = ManuallyDrop::new(Arc::from_raw(account_manager));
|
||||
let cmd_api = block_on(deltachat_jsonrpc::api::CommandApi::from_arc(Arc::clone(
|
||||
&account_manager,
|
||||
)));
|
||||
|
||||
let (request_handle, receiver) = RpcClient::new();
|
||||
let handle = RpcSession::new(request_handle, cmd_api);
|
||||
|
||||
@@ -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.23.0"
|
||||
version = "2.46.0-dev"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
@@ -19,7 +19,6 @@ yerpc = { workspace = true, features = ["anyhow_expose", "openrpc"] }
|
||||
typescript-type-def = { version = "0.5.13", features = ["json_value"] }
|
||||
tokio = { workspace = true }
|
||||
sanitize-filename = { workspace = true }
|
||||
walkdir = "2.5.0"
|
||||
base64 = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -10,37 +10,38 @@ pub use deltachat::accounts::Accounts;
|
||||
use deltachat::blob::BlobObject;
|
||||
use deltachat::calls::ice_servers;
|
||||
use deltachat::chat::{
|
||||
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
|
||||
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
|
||||
self, add_contact_to_chat, forward_msgs, forward_msgs_2ctx, get_chat_media, get_chat_msgs,
|
||||
get_chat_msgs_ex, marknoticed_all_chats, marknoticed_chat, remove_contact_from_chat, Chat,
|
||||
ChatId, ChatItem, MessageListOptions,
|
||||
};
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::config::Config;
|
||||
use deltachat::config::{get_all_ui_config_keys, Config};
|
||||
use deltachat::constants::DC_MSG_ID_DAYMARKER;
|
||||
use deltachat::contact::{may_be_valid_addr, Contact, ContactId, Origin};
|
||||
use deltachat::context::get_info;
|
||||
use deltachat::ephemeral::Timer;
|
||||
use deltachat::imex;
|
||||
use deltachat::location;
|
||||
use deltachat::message::get_msg_read_receipts;
|
||||
use deltachat::message::{
|
||||
self, delete_msgs_ex, markseen_msgs, Message, MessageState, MsgId, Viewtype,
|
||||
self, delete_msgs_ex, get_existing_msg_ids, get_msg_read_receipt_count, get_msg_read_receipts,
|
||||
markseen_msgs, Message, MessageState, MsgId, Viewtype,
|
||||
};
|
||||
use deltachat::peer_channels::{
|
||||
leave_webxdc_realtime, send_webxdc_realtime_advertisement, send_webxdc_realtime_data,
|
||||
};
|
||||
use deltachat::provider::get_provider_info;
|
||||
use deltachat::qr::{self, Qr};
|
||||
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
|
||||
use deltachat::qr_code_generator::{create_qr_svg, generate_backup_qr, get_securejoin_qr_svg};
|
||||
use deltachat::reaction::{get_msg_reactions, send_reaction};
|
||||
use deltachat::securejoin;
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::storage_usage::{get_blobdir_storage_usage, get_storage_usage};
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::EventEmitter;
|
||||
use sanitize_filename::is_sanitized;
|
||||
use tokio::fs;
|
||||
use tokio::sync::{watch, Mutex, RwLock};
|
||||
use types::login_param::EnteredLoginParam;
|
||||
use walkdir::WalkDir;
|
||||
use yerpc::rpc;
|
||||
|
||||
pub mod types;
|
||||
@@ -120,14 +121,14 @@ impl CommandApi {
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_context_opt(&self, id: u32) -> Option<deltachat::context::Context> {
|
||||
self.accounts.read().await.get_account(id)
|
||||
}
|
||||
|
||||
async fn get_context(&self, id: u32) -> Result<deltachat::context::Context> {
|
||||
let sc = self
|
||||
.accounts
|
||||
.read()
|
||||
self.get_context_opt(id)
|
||||
.await
|
||||
.get_account(id)
|
||||
.ok_or_else(|| anyhow!("account with id {id} not found"))?;
|
||||
Ok(sc)
|
||||
.ok_or_else(|| anyhow!("account with id {id} not found"))
|
||||
}
|
||||
|
||||
async fn with_state<F, T>(&self, id: u32, with_state: F) -> T
|
||||
@@ -193,6 +194,16 @@ impl CommandApi {
|
||||
.context("event channel is closed")
|
||||
}
|
||||
|
||||
/// Waits for at least one event and return a batch of events.
|
||||
async fn get_next_event_batch(&self) -> Vec<Event> {
|
||||
self.event_emitter
|
||||
.recv_batch()
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|event| event.into())
|
||||
.collect()
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// Account Management
|
||||
// ---------------------------------------------
|
||||
@@ -273,7 +284,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 +294,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
|
||||
// ---------------------------------------------
|
||||
@@ -323,13 +339,7 @@ impl CommandApi {
|
||||
async fn get_account_file_size(&self, account_id: u32) -> Result<u64> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let dbfile = ctx.get_dbfile().metadata()?.len();
|
||||
let total_size = WalkDir::new(ctx.get_blobdir())
|
||||
.max_depth(2)
|
||||
.into_iter()
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter_map(|entry| entry.metadata().ok())
|
||||
.filter(|metadata| metadata.is_file())
|
||||
.fold(0, |acc, m| acc + m.len());
|
||||
let total_size = get_blobdir_storage_usage(&ctx);
|
||||
|
||||
Ok(dbfile + total_size)
|
||||
}
|
||||
@@ -361,6 +371,13 @@ impl CommandApi {
|
||||
ctx.get_info().await
|
||||
}
|
||||
|
||||
/// Get storage usage report as formatted string
|
||||
async fn get_storage_usage_report_string(&self, account_id: u32) -> Result<String> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let storage_usage = get_storage_usage(&ctx).await?;
|
||||
Ok(storage_usage.to_string())
|
||||
}
|
||||
|
||||
/// Get the blob dir.
|
||||
async fn get_blob_dir(&self, account_id: u32) -> Result<Option<String>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
@@ -409,11 +426,11 @@ impl CommandApi {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set configuration values from a QR code. (technically from the URI that is stored in the qrcode)
|
||||
/// Before this function is called, `checkQr()` should confirm the type of the
|
||||
/// QR code is `account` or `webrtcInstance`.
|
||||
/// Set configuration values from a QR code (technically from the URI stored in it).
|
||||
/// Before this function is called, `check_qr()` should be used to get the QR code type.
|
||||
///
|
||||
/// Internally, the function will call dc_set_config() with the appropriate keys,
|
||||
/// "DCACCOUNT:" and "DCLOGIN:" QR codes configure the account, but I/O mustn't be started for
|
||||
/// such QR codes, consider using [`Self::add_transport_from_qr`] which also restarts I/O.
|
||||
async fn set_config_from_qr(&self, account_id: u32, qr_content: String) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
qr::set_config_from_qr(&ctx, &qr_content).await
|
||||
@@ -445,6 +462,12 @@ impl CommandApi {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Returns all `ui.*` config keys that were set by the UI.
|
||||
async fn get_all_ui_config_keys(&self, account_id: u32) -> Result<Vec<String>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
get_all_ui_config_keys(&ctx).await
|
||||
}
|
||||
|
||||
async fn set_stock_strings(&self, strings: HashMap<u32, String>) -> Result<()> {
|
||||
let accounts = self.accounts.read().await;
|
||||
for (stock_id, stock_message) in strings {
|
||||
@@ -788,11 +811,11 @@ impl CommandApi {
|
||||
/// Delete a chat.
|
||||
///
|
||||
/// Messages are deleted from the device and the chat database entry is deleted.
|
||||
/// After that, the event #DC_EVENT_MSGS_CHANGED is posted.
|
||||
/// After that, a `MsgsChanged` event is emitted.
|
||||
/// Messages are deleted from the server in background.
|
||||
///
|
||||
/// Things that are _not done_ implicitly:
|
||||
///
|
||||
/// - Messages are **not deleted from the server**.
|
||||
/// - The chat or the contact is **not blocked**, so new messages from the user/the group may appear as a contact request
|
||||
/// and the user may create the chat again.
|
||||
/// - **Groups are not left** - this would
|
||||
@@ -841,6 +864,8 @@ impl CommandApi {
|
||||
/// if `checkQr()` returns `askVerifyContact` or `askVerifyGroup`
|
||||
/// an out-of-band-verification can be joined using `secure_join()`
|
||||
///
|
||||
/// @deprecated as of 2026-03; use create_qr_svg(get_chat_securejoin_qr_code()) instead.
|
||||
///
|
||||
/// chat_id: If set to a group-chat-id,
|
||||
/// the Verified-Group-Invite protocol is offered in the QR code;
|
||||
/// works for protected groups as well as for normal groups.
|
||||
@@ -1030,7 +1055,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,
|
||||
@@ -1055,7 +1080,8 @@ impl CommandApi {
|
||||
/// Set group name.
|
||||
///
|
||||
/// If the group is already _promoted_ (any message was sent to the group),
|
||||
/// all group members are informed by a special status message that is sent automatically by this function.
|
||||
/// or if this is a brodacast channel,
|
||||
/// all members are informed by a special status message that is sent automatically by this function.
|
||||
///
|
||||
/// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
async fn set_chat_name(&self, account_id: u32, chat_id: u32, new_name: String) -> Result<()> {
|
||||
@@ -1063,10 +1089,39 @@ impl CommandApi {
|
||||
chat::set_chat_name(&ctx, ChatId::new(chat_id), &new_name).await
|
||||
}
|
||||
|
||||
/// Set group or broadcast channel description.
|
||||
///
|
||||
/// If the group is already _promoted_ (any message was sent to the group),
|
||||
/// or if this is a brodacast channel,
|
||||
/// all members are informed by a special status message that is sent automatically by this function.
|
||||
///
|
||||
/// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
///
|
||||
/// See also [`Self::get_chat_description`] / `getChatDescription()`.
|
||||
async fn set_chat_description(
|
||||
&self,
|
||||
account_id: u32,
|
||||
chat_id: u32,
|
||||
description: String,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
chat::set_chat_description(&ctx, ChatId::new(chat_id), &description).await
|
||||
}
|
||||
|
||||
/// Load the chat description from the database.
|
||||
///
|
||||
/// UIs show this in the profile page of the chat,
|
||||
/// it is settable by [`Self::set_chat_description`] / `setChatDescription()`.
|
||||
async fn get_chat_description(&self, account_id: u32, chat_id: u32) -> Result<String> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
chat::get_chat_description(&ctx, ChatId::new(chat_id)).await
|
||||
}
|
||||
|
||||
/// Set group profile image.
|
||||
///
|
||||
/// If the group is already _promoted_ (any message was sent to the group),
|
||||
/// all group members are informed by a special status message that is sent automatically by this function.
|
||||
/// or if this is a brodacast channel,
|
||||
/// all members are informed by a special status message that is sent automatically by this function.
|
||||
///
|
||||
/// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
///
|
||||
@@ -1151,10 +1206,24 @@ impl CommandApi {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Mark all messages in all chats as _noticed_.
|
||||
/// Skips messages from blocked contacts, but does not skip messages in muted chats.
|
||||
///
|
||||
/// _Noticed_ messages are no longer _fresh_ and do not count as being unseen
|
||||
/// but are still waiting for being marked as "seen" using markseen_msgs()
|
||||
/// (read receipts aren't sent for noticed messages).
|
||||
///
|
||||
/// Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED.
|
||||
/// See also markseen_msgs().
|
||||
pub async fn marknoticed_all_chats(&self, account_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
marknoticed_all_chats(&ctx).await
|
||||
}
|
||||
|
||||
/// Mark all messages in a chat as _noticed_.
|
||||
/// _Noticed_ messages are no longer _fresh_ and do not count as being unseen
|
||||
/// but are still waiting for being marked as "seen" using markseen_msgs()
|
||||
/// (IMAP/MDNs is not done for noticed messages).
|
||||
/// (read receipts aren't sent for noticed messages).
|
||||
///
|
||||
/// Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED.
|
||||
/// See also markseen_msgs().
|
||||
@@ -1290,6 +1359,24 @@ impl CommandApi {
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Checks if the messages with given IDs exist.
|
||||
///
|
||||
/// Returns IDs of existing messages.
|
||||
async fn get_existing_msg_ids(&self, account_id: u32, msg_ids: Vec<u32>) -> Result<Vec<u32>> {
|
||||
if let Some(context) = self.get_context_opt(account_id).await {
|
||||
let msg_ids: Vec<MsgId> = msg_ids.into_iter().map(MsgId::new).collect();
|
||||
let existing_msg_ids = get_existing_msg_ids(&context, &msg_ids).await?;
|
||||
Ok(existing_msg_ids
|
||||
.into_iter()
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.collect())
|
||||
} else {
|
||||
// Account does not exist, so messages do not exist either,
|
||||
// but this is not an error.
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_message_list_items(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -1403,6 +1490,18 @@ impl CommandApi {
|
||||
MessageInfo::from_msg_id(&ctx, MsgId::new(message_id)).await
|
||||
}
|
||||
|
||||
/// Returns count of read receipts on message.
|
||||
///
|
||||
/// This view count is meant as a feedback measure for the channel owner only.
|
||||
async fn get_message_read_receipt_count(
|
||||
&self,
|
||||
account_id: u32,
|
||||
message_id: u32,
|
||||
) -> Result<usize> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
get_msg_read_receipt_count(&ctx, MsgId::new(message_id)).await
|
||||
}
|
||||
|
||||
/// Returns contacts that sent read receipts and the time of reading.
|
||||
async fn get_message_read_receipts(
|
||||
&self,
|
||||
@@ -1883,6 +1982,8 @@ impl CommandApi {
|
||||
/// even if there is no concurrent call to [`CommandApi::provide_backup`],
|
||||
/// but will fail after 60 seconds to avoid deadlocks.
|
||||
///
|
||||
/// @deprecated as of 2026-03; use `create_qr_svg(get_backup_qr())` instead.
|
||||
///
|
||||
/// Returns the QR code rendered as an SVG image.
|
||||
async fn get_backup_qr_svg(&self, account_id: u32) -> Result<String> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
@@ -1896,6 +1997,11 @@ impl CommandApi {
|
||||
generate_backup_qr(&ctx, &qr).await
|
||||
}
|
||||
|
||||
/// Renders the given text as a QR code SVG image.
|
||||
async fn create_qr_svg(&self, text: String) -> Result<String> {
|
||||
create_qr_svg(&text)
|
||||
}
|
||||
|
||||
/// Gets a backup from a remote provider.
|
||||
///
|
||||
/// This retrieves the backup from a remote device over the network and imports it into
|
||||
@@ -2110,10 +2216,11 @@ impl CommandApi {
|
||||
account_id: u32,
|
||||
chat_id: u32,
|
||||
place_call_info: String,
|
||||
has_video: bool,
|
||||
) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let msg_id = ctx
|
||||
.place_outgoing_call(ChatId::new(chat_id), place_call_info)
|
||||
.place_outgoing_call(ChatId::new(chat_id), place_call_info, has_video)
|
||||
.await?;
|
||||
Ok(msg_id.to_u32())
|
||||
}
|
||||
@@ -2177,6 +2284,27 @@ impl CommandApi {
|
||||
forward_msgs(&ctx, &message_ids, ChatId::new(chat_id)).await
|
||||
}
|
||||
|
||||
/// Forward messages to a chat in another account.
|
||||
/// See [`Self::forward_messages`] for more info.
|
||||
async fn forward_messages_to_account(
|
||||
&self,
|
||||
src_account_id: u32,
|
||||
src_message_ids: Vec<u32>,
|
||||
dst_account_id: u32,
|
||||
dst_chat_id: u32,
|
||||
) -> Result<()> {
|
||||
let src_ctx = self.get_context(src_account_id).await?;
|
||||
let dst_ctx = self.get_context(dst_account_id).await?;
|
||||
let src_message_ids: Vec<MsgId> = src_message_ids.into_iter().map(MsgId::new).collect();
|
||||
forward_msgs_2ctx(
|
||||
&src_ctx,
|
||||
&src_message_ids,
|
||||
&dst_ctx,
|
||||
ChatId::new(dst_chat_id),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -2387,7 +2515,10 @@ impl CommandApi {
|
||||
continue;
|
||||
}
|
||||
let sticker_name = sticker_entry.file_name().into_string().unwrap_or_default();
|
||||
if sticker_name.ends_with(".png") || sticker_name.ends_with(".webp") {
|
||||
if sticker_name.ends_with(".png")
|
||||
|| sticker_name.ends_with(".webp")
|
||||
|| sticker_name.ends_with(".gif")
|
||||
{
|
||||
sticker_paths.push(
|
||||
sticker_entry
|
||||
.path()
|
||||
|
||||
@@ -15,7 +15,7 @@ pub enum Account {
|
||||
display_name: Option<String>,
|
||||
addr: Option<String>,
|
||||
// size: u32,
|
||||
profile_image: Option<String>, // TODO: This needs to be converted to work with blob http server.
|
||||
profile_image: Option<String>,
|
||||
color: String,
|
||||
/// Optional tag as "Work", "Family".
|
||||
/// Meant to help profile owner to differ between profiles with similar names.
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
|
||||
use deltachat::calls::{call_state, sdp_has_video, CallState};
|
||||
use deltachat::calls::{call_state, CallState};
|
||||
use deltachat::context::Context;
|
||||
use deltachat::message::MsgId;
|
||||
use serde::Serialize;
|
||||
@@ -15,7 +15,7 @@ pub struct JsonrpcCallInfo {
|
||||
/// even if incoming call event was missed.
|
||||
pub sdp_offer: String,
|
||||
|
||||
/// True if SDP offer has a video.
|
||||
/// True if the call is started as a video call.
|
||||
pub has_video: bool,
|
||||
|
||||
/// Call state.
|
||||
@@ -30,7 +30,7 @@ impl JsonrpcCallInfo {
|
||||
format!("Attempting to get call state of non-call message {msg_id}")
|
||||
})?;
|
||||
let sdp_offer = call_info.place_call_info.clone();
|
||||
let has_video = sdp_has_video(&sdp_offer).unwrap_or_default();
|
||||
let has_video = call_info.has_video_initially();
|
||||
let state = JsonrpcCallState::from_msg_id(context, msg_id).await?;
|
||||
|
||||
Ok(JsonrpcCallInfo {
|
||||
|
||||
@@ -6,12 +6,10 @@ 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;
|
||||
|
||||
use super::color_int_to_hex_string;
|
||||
use super::contact::ContactObject;
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -46,10 +44,9 @@ 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>,
|
||||
contact_ids: Vec<u32>,
|
||||
|
||||
/// Contact IDs of the past chat members.
|
||||
@@ -70,7 +67,7 @@ pub struct FullChat {
|
||||
// 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
|
||||
ephemeral_timer: u32,
|
||||
can_send: bool,
|
||||
was_seen_recently: bool,
|
||||
mailing_list_address: Option<String>,
|
||||
@@ -84,20 +81,6 @@ impl FullChat {
|
||||
let contact_ids = get_chat_contacts(context, rust_chat_id).await?;
|
||||
let past_contact_ids = get_past_chat_contacts(context, rust_chat_id).await?;
|
||||
|
||||
let mut contacts = Vec::with_capacity(contact_ids.len());
|
||||
|
||||
for contact_id in &contact_ids {
|
||||
contacts.push(
|
||||
ContactObject::try_from_dc_contact(
|
||||
context,
|
||||
Contact::get_by_id(context, *contact_id)
|
||||
.await
|
||||
.context("failed to load contact")?,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
let profile_image = match chat.get_profile_image(context).await? {
|
||||
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
|
||||
None => None,
|
||||
@@ -130,10 +113,9 @@ impl FullChat {
|
||||
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,
|
||||
contact_ids: contact_ids.iter().map(|id| id.to_u32()).collect(),
|
||||
past_contact_ids: past_contact_ids.iter().map(|id| id.to_u32()).collect(),
|
||||
color,
|
||||
@@ -151,7 +133,6 @@ impl FullChat {
|
||||
}
|
||||
|
||||
/// cheaper version of fullchat, omits:
|
||||
/// - contacts
|
||||
/// - contact_ids
|
||||
/// - fresh_message_counter
|
||||
/// - ephemeral_timer
|
||||
@@ -192,7 +173,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,
|
||||
@@ -220,7 +201,7 @@ impl BasicChat {
|
||||
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,
|
||||
@@ -274,3 +255,37 @@ impl JsonrpcChatVisibility {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,7 +24,7 @@ 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,
|
||||
@@ -151,7 +152,7 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
name: chat.get_name().to_owned(),
|
||||
avatar_path,
|
||||
color,
|
||||
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
||||
chat_type: chat.get_type().into(),
|
||||
last_updated,
|
||||
summary_text1,
|
||||
summary_text2,
|
||||
|
||||
@@ -47,8 +47,7 @@ pub struct ContactObject {
|
||||
///
|
||||
/// - If `verifierId` != 0,
|
||||
/// display text "Introduced by ..."
|
||||
/// with the name and address of the contact
|
||||
/// formatted by `name_and_addr`/`nameAndAddr`.
|
||||
/// with the name of the contact.
|
||||
/// Prefix the text by a green checkmark.
|
||||
///
|
||||
/// - If `verifierId` == 0 and `isVerified` != 0,
|
||||
|
||||
@@ -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 {
|
||||
@@ -270,7 +271,7 @@ pub enum EventType {
|
||||
/// Progress.
|
||||
///
|
||||
/// 0=error, 1-999=progress in permille, 1000=success and done
|
||||
progress: usize,
|
||||
progress: u16,
|
||||
|
||||
/// Progress comment or error, something to display to the user.
|
||||
comment: Option<String>,
|
||||
@@ -281,7 +282,7 @@ pub enum EventType {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ImexProgress {
|
||||
/// 0=error, 1-999=progress in permille, 1000=success and done
|
||||
progress: usize,
|
||||
progress: u16,
|
||||
},
|
||||
|
||||
/// A file has been exported. A file has been written by imex().
|
||||
@@ -307,12 +308,12 @@ 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,
|
||||
|
||||
/// Progress, always 1000.
|
||||
progress: usize,
|
||||
progress: u16,
|
||||
},
|
||||
|
||||
/// Progress information of a secure-join handshake from the view of the joiner
|
||||
@@ -328,7 +329,7 @@ pub enum EventType {
|
||||
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
||||
/// (Bob has verified alice and waits until Alice does the same for him)
|
||||
/// 1000=vg-member-added/vc-contact-confirm received
|
||||
progress: usize,
|
||||
progress: u16,
|
||||
},
|
||||
|
||||
/// The connectivity to the server changed.
|
||||
@@ -440,6 +441,8 @@ pub enum EventType {
|
||||
msg_id: u32,
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: u32,
|
||||
/// The call was accepted from this device (process).
|
||||
from_this_device: bool,
|
||||
},
|
||||
|
||||
/// Outgoing call accepted.
|
||||
@@ -459,6 +462,15 @@ pub enum EventType {
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: u32,
|
||||
},
|
||||
|
||||
/// One or more transports has changed.
|
||||
///
|
||||
/// UI should update the list.
|
||||
///
|
||||
/// This event is emitted when transport
|
||||
/// synchronization messages arrives,
|
||||
/// but not when the UI modifies the transport list by itself.
|
||||
TransportsModified,
|
||||
}
|
||||
|
||||
impl From<CoreEventType> for EventType {
|
||||
@@ -570,7 +582,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,
|
||||
},
|
||||
@@ -624,9 +636,14 @@ impl From<CoreEventType> for EventType {
|
||||
place_call_info,
|
||||
has_video,
|
||||
},
|
||||
CoreEventType::IncomingCallAccepted { msg_id, chat_id } => IncomingCallAccepted {
|
||||
CoreEventType::IncomingCallAccepted {
|
||||
msg_id,
|
||||
chat_id,
|
||||
from_this_device,
|
||||
} => IncomingCallAccepted {
|
||||
msg_id: msg_id.to_u32(),
|
||||
chat_id: chat_id.to_u32(),
|
||||
from_this_device,
|
||||
},
|
||||
CoreEventType::OutgoingCallAccepted {
|
||||
msg_id,
|
||||
@@ -641,6 +658,8 @@ impl From<CoreEventType> for EventType {
|
||||
msg_id: msg_id.to_u32(),
|
||||
chat_id: chat_id.to_u32(),
|
||||
},
|
||||
CoreEventType::TransportsModified => TransportsModified,
|
||||
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||
|
||||
@@ -16,6 +16,7 @@ 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;
|
||||
@@ -91,6 +92,9 @@ pub struct MessageObject {
|
||||
|
||||
file: Option<String>,
|
||||
file_mime: Option<String>,
|
||||
|
||||
/// The size of the file in bytes, if applicable.
|
||||
/// If message is a pre-message, then this is the size of the file to be downloaded.
|
||||
file_bytes: u64,
|
||||
file_name: Option<String>,
|
||||
|
||||
@@ -384,6 +388,7 @@ impl From<download::DownloadState> for DownloadState {
|
||||
pub enum SystemMessageType {
|
||||
Unknown,
|
||||
GroupNameChanged,
|
||||
GroupDescriptionChanged,
|
||||
GroupImageChanged,
|
||||
MemberAddedToGroup,
|
||||
MemberRemovedFromGroup,
|
||||
@@ -436,6 +441,7 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
||||
match system_message_type {
|
||||
SystemMessage::Unknown => SystemMessageType::Unknown,
|
||||
SystemMessage::GroupNameChanged => SystemMessageType::GroupNameChanged,
|
||||
SystemMessage::GroupDescriptionChanged => SystemMessageType::GroupDescriptionChanged,
|
||||
SystemMessage::GroupImageChanged => SystemMessageType::GroupImageChanged,
|
||||
SystemMessage::MemberAddedToGroup => SystemMessageType::MemberAddedToGroup,
|
||||
SystemMessage::MemberRemovedFromGroup => SystemMessageType::MemberRemovedFromGroup,
|
||||
@@ -531,7 +537,7 @@ pub struct MessageSearchResult {
|
||||
chat_profile_image: Option<String>,
|
||||
chat_color: String,
|
||||
chat_name: String,
|
||||
chat_type: u32,
|
||||
chat_type: JsonrpcChatType,
|
||||
is_chat_contact_request: bool,
|
||||
is_chat_archived: bool,
|
||||
message: String,
|
||||
@@ -569,7 +575,7 @@ 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_contact_request: chat.is_contact_request(),
|
||||
is_chat_archived: chat.get_visibility() == ChatVisibility::Archived,
|
||||
|
||||
@@ -19,6 +19,8 @@ pub enum QrObject {
|
||||
invitenumber: String,
|
||||
/// Authentication code.
|
||||
authcode: String,
|
||||
/// Whether the inviter supports the new Securejoin v3 protocol
|
||||
is_v3: bool,
|
||||
},
|
||||
/// Ask the user whether to join the group.
|
||||
AskVerifyGroup {
|
||||
@@ -34,6 +36,30 @@ pub enum QrObject {
|
||||
invitenumber: String,
|
||||
/// Authentication code.
|
||||
authcode: String,
|
||||
/// Whether the inviter supports the new Securejoin v3 protocol
|
||||
is_v3: bool,
|
||||
},
|
||||
/// 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,
|
||||
/// Whether the inviter supports the new Securejoin v3 protocol
|
||||
is_v3: bool,
|
||||
},
|
||||
/// Contact fingerprint is verified.
|
||||
///
|
||||
@@ -137,6 +163,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.
|
||||
@@ -163,6 +204,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.
|
||||
@@ -179,6 +235,7 @@ impl From<Qr> for QrObject {
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
is_v3,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
@@ -187,6 +244,7 @@ impl From<Qr> for QrObject {
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
is_v3,
|
||||
}
|
||||
}
|
||||
Qr::AskVerifyGroup {
|
||||
@@ -196,6 +254,7 @@ impl From<Qr> for QrObject {
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
is_v3,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
@@ -206,6 +265,28 @@ impl From<Qr> for QrObject {
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
is_v3,
|
||||
}
|
||||
}
|
||||
Qr::AskJoinBroadcast {
|
||||
name,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
authcode,
|
||||
invitenumber,
|
||||
is_v3,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::AskJoinBroadcast {
|
||||
name,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
authcode,
|
||||
invitenumber,
|
||||
is_v3,
|
||||
}
|
||||
}
|
||||
Qr::FprOk { contact_id } => {
|
||||
@@ -267,6 +348,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,
|
||||
@@ -301,6 +401,25 @@ 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 },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.23.0"
|
||||
"version": "2.46.0-dev"
|
||||
}
|
||||
|
||||
@@ -45,15 +45,30 @@ const constants = data
|
||||
key.startsWith("DC_SOCKET_") ||
|
||||
key.startsWith("DC_LP_AUTH_") ||
|
||||
key.startsWith("DC_PUSH_") ||
|
||||
key.startsWith("DC_TEXT1_")
|
||||
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`,
|
||||
);
|
||||
|
||||
@@ -53,18 +53,19 @@ export class BaseDeltaChat<
|
||||
*/
|
||||
async eventLoop(): Promise<void> {
|
||||
while (true) {
|
||||
const event = await this.rpc.getNextEvent();
|
||||
//@ts-ignore
|
||||
this.emit(event.event.kind, event.contextId, event.event);
|
||||
this.emit("ALL", event.contextId, event.event);
|
||||
for (const event of await this.rpc.getNextEventBatch()) {
|
||||
//@ts-ignore
|
||||
this.emit(event.event.kind, event.contextId, event.event);
|
||||
this.emit("ALL", event.contextId, event.event);
|
||||
|
||||
if (this.contextEmitters[event.contextId]) {
|
||||
this.contextEmitters[event.contextId].emit(
|
||||
event.event.kind,
|
||||
//@ts-ignore
|
||||
event.event as any,
|
||||
);
|
||||
this.contextEmitters[event.contextId].emit("ALL", event.event as any);
|
||||
if (this.contextEmitters[event.contextId]) {
|
||||
this.contextEmitters[event.contextId].emit(
|
||||
event.event.kind,
|
||||
//@ts-ignore
|
||||
event.event as any,
|
||||
);
|
||||
this.contextEmitters[event.contextId].emit("ALL", event.event as any);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ describe("online tests", function () {
|
||||
await dc.rpc.setConfig(accountId1, "addr", account1.email);
|
||||
await dc.rpc.setConfig(accountId1, "mail_pw", account1.password);
|
||||
await dc.rpc.configure(accountId1);
|
||||
await waitForEvent(dc, "ImapInboxIdle", accountId1);
|
||||
|
||||
accountId2 = await dc.rpc.addAccount();
|
||||
await dc.rpc.batchSetConfig(accountId2, {
|
||||
@@ -71,6 +72,7 @@ describe("online tests", function () {
|
||||
mail_pw: account2.password,
|
||||
});
|
||||
await dc.rpc.configure(accountId2);
|
||||
await waitForEvent(dc, "ImapInboxIdle", accountId2);
|
||||
accountsConfigured = true;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "2.23.0"
|
||||
version = "2.46.0-dev"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
@@ -343,6 +343,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
addmember <contact-id>\n\
|
||||
removemember <contact-id>\n\
|
||||
groupname <name>\n\
|
||||
groupdescription <description>\n\
|
||||
groupimage <image>\n\
|
||||
chatinfo\n\
|
||||
sendlocations <seconds>\n\
|
||||
@@ -770,6 +771,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
|
||||
println!("Chat name set");
|
||||
}
|
||||
"groupdescription" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
ensure!(!arg1.is_empty(), "Argument <description> missing.");
|
||||
chat::set_chat_description(&context, sel_chat.as_ref().unwrap().get_id(), arg1).await?;
|
||||
|
||||
println!("Chat description set");
|
||||
}
|
||||
"groupimage" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
ensure!(!arg1.is_empty(), "Argument <image> missing.");
|
||||
@@ -1231,7 +1239,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"setqr" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
||||
match set_config_from_qr(&context, arg1).await {
|
||||
Ok(()) => println!("Config set from QR code, you can now call 'configure'"),
|
||||
Ok(()) => eprintln!("Config set from the QR code."),
|
||||
Err(err) => eprintln!("Cannot set config from QR code: {err:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ const DB_COMMANDS: [&str; 11] = [
|
||||
"housekeeping",
|
||||
];
|
||||
|
||||
const CHAT_COMMANDS: [&str; 39] = [
|
||||
const CHAT_COMMANDS: [&str; 40] = [
|
||||
"listchats",
|
||||
"listarchived",
|
||||
"start-realtime",
|
||||
@@ -192,6 +192,7 @@ const CHAT_COMMANDS: [&str; 39] = [
|
||||
"addmember",
|
||||
"removemember",
|
||||
"groupname",
|
||||
"groupdescription",
|
||||
"groupimage",
|
||||
"chatinfo",
|
||||
"sendlocations",
|
||||
@@ -430,12 +431,12 @@ async fn handle_cmd(
|
||||
}
|
||||
"oauth2" => {
|
||||
if let Some(addr) = ctx.get_config(config::Config::Addr).await? {
|
||||
let oauth2_url =
|
||||
get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await?;
|
||||
if oauth2_url.is_none() {
|
||||
println!("OAuth2 not available for {}.", &addr);
|
||||
if let Some(oauth2_url) =
|
||||
get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await?
|
||||
{
|
||||
println!("Open the following url, set mail_pw to the generated token and server_flags to 2:\n{oauth2_url}");
|
||||
} else {
|
||||
println!("Open the following url, set mail_pw to the generated token and server_flags to 2:\n{}", oauth2_url.unwrap());
|
||||
println!("OAuth2 not available for {}.", &addr);
|
||||
}
|
||||
} else {
|
||||
println!("oauth2: set addr first.");
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
RPC client connects to standalone Delta Chat RPC server `deltachat-rpc-server`
|
||||
and provides asynchronous interface to it.
|
||||
`rpc.start()` performs a health-check RPC call to verify the server
|
||||
started successfully and will raise an error if startup fails
|
||||
(e.g. if the accounts directory could not be used).
|
||||
|
||||
## Getting started
|
||||
|
||||
@@ -30,6 +33,15 @@ $ pip install .
|
||||
|
||||
Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.
|
||||
|
||||
|
||||
## Activating current checkout of deltachat-rpc-client and -server for development
|
||||
|
||||
Go to root repository directory and run:
|
||||
```
|
||||
$ scripts/make-rpc-testenv.sh
|
||||
$ source venv/bin/activate
|
||||
```
|
||||
|
||||
## Using in REPL
|
||||
|
||||
Setup a development environment:
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=45"]
|
||||
requires = ["setuptools>=77"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "2.23.0"
|
||||
version = "2.46.0-dev"
|
||||
license = "MPL-2.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Operating System :: MacOS :: MacOS X",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
@@ -24,7 +22,7 @@ classifiers = [
|
||||
"Topic :: Communications :: Email"
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.8"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
deltachat_rpc_client = [
|
||||
|
||||
@@ -8,7 +8,7 @@ from .const import EventType, SpecialContactId
|
||||
from .contact import Contact
|
||||
from .deltachat import DeltaChat
|
||||
from .message import Message
|
||||
from .rpc import Rpc
|
||||
from .rpc import JsonRpcError, Rpc
|
||||
|
||||
__all__ = [
|
||||
"Account",
|
||||
@@ -19,6 +19,7 @@ __all__ = [
|
||||
"Contact",
|
||||
"DeltaChat",
|
||||
"EventType",
|
||||
"JsonRpcError",
|
||||
"Message",
|
||||
"SpecialContactId",
|
||||
"Rpc",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import argparse
|
||||
import functools
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
@@ -44,8 +45,13 @@ class AttrDict(dict):
|
||||
super().__setattr__(attr, val)
|
||||
|
||||
|
||||
def _forever(_event: AttrDict) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def run_client_cli(
|
||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||
until: Callable[[AttrDict], bool] = _forever,
|
||||
argv: Optional[list] = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
@@ -55,10 +61,11 @@ def run_client_cli(
|
||||
"""
|
||||
from .client import Client
|
||||
|
||||
_run_cli(Client, hooks, argv, **kwargs)
|
||||
_run_cli(Client, until, hooks, argv, **kwargs)
|
||||
|
||||
|
||||
def run_bot_cli(
|
||||
until: Callable[[AttrDict], bool] = _forever,
|
||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||
argv: Optional[list] = None,
|
||||
**kwargs,
|
||||
@@ -69,11 +76,12 @@ def run_bot_cli(
|
||||
"""
|
||||
from .client import Bot
|
||||
|
||||
_run_cli(Bot, hooks, argv, **kwargs)
|
||||
_run_cli(Bot, until, hooks, argv, **kwargs)
|
||||
|
||||
|
||||
def _run_cli(
|
||||
client_type: Type["Client"],
|
||||
until: Callable[[AttrDict], bool] = _forever,
|
||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||
argv: Optional[list] = None,
|
||||
**kwargs,
|
||||
@@ -111,7 +119,7 @@ def _run_cli(
|
||||
kwargs={"email": args.email, "password": args.password},
|
||||
)
|
||||
configure_thread.start()
|
||||
client.run_forever()
|
||||
client.run_until(until)
|
||||
|
||||
|
||||
def extract_addr(text: str) -> str:
|
||||
@@ -179,6 +187,7 @@ class futuremethod: # noqa: N801
|
||||
"""Decorator for async methods."""
|
||||
|
||||
def __init__(self, func):
|
||||
functools.update_wrapper(self, func)
|
||||
self._func = func
|
||||
|
||||
def __get__(self, instance, owner=None):
|
||||
|
||||
@@ -130,6 +130,10 @@ class Account:
|
||||
"""Add a new transport using a QR code."""
|
||||
yield self._rpc.add_transport_from_qr.future(self.id, qr)
|
||||
|
||||
def delete_transport(self, addr: str):
|
||||
"""Delete a transport."""
|
||||
self._rpc.delete_transport(self.id, addr)
|
||||
|
||||
@futuremethod
|
||||
def list_transports(self):
|
||||
"""Return the list of all email accounts that are used as a transport in the current profile."""
|
||||
@@ -326,7 +330,7 @@ class Account:
|
||||
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,
|
||||
@@ -399,9 +403,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):
|
||||
@@ -416,12 +421,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."""
|
||||
|
||||
@@ -219,10 +219,12 @@ class Chat:
|
||||
"""Mark all messages in this chat as noticed."""
|
||||
self._rpc.marknoticed_chat(self.account.id, self.id)
|
||||
|
||||
def add_contact(self, *contact: Union[int, str, Contact]) -> None:
|
||||
def add_contact(self, *contact: Union[int, str, Contact, "Account"]) -> None:
|
||||
"""Add contacts to this group."""
|
||||
from .account import Account
|
||||
|
||||
for cnt in contact:
|
||||
if isinstance(cnt, str):
|
||||
if isinstance(cnt, (str, Account)):
|
||||
contact_id = self.account.create_contact(cnt).id
|
||||
elif not isinstance(cnt, int):
|
||||
contact_id = cnt.id
|
||||
@@ -230,10 +232,12 @@ class Chat:
|
||||
contact_id = cnt
|
||||
self._rpc.add_contact_to_chat(self.account.id, self.id, contact_id)
|
||||
|
||||
def remove_contact(self, *contact: Union[int, str, Contact]) -> None:
|
||||
def remove_contact(self, *contact: Union[int, str, Contact, "Account"]) -> None:
|
||||
"""Remove members from this group."""
|
||||
from .account import Account
|
||||
|
||||
for cnt in contact:
|
||||
if isinstance(cnt, str):
|
||||
if isinstance(cnt, (str, Account)):
|
||||
contact_id = self.account.create_contact(cnt).id
|
||||
elif not isinstance(cnt, int):
|
||||
contact_id = cnt.id
|
||||
@@ -249,6 +253,10 @@ class Chat:
|
||||
contacts = self._rpc.get_chat_contacts(self.account.id, self.id)
|
||||
return [Contact(self.account, contact_id) for contact_id in contacts]
|
||||
|
||||
def num_contacts(self) -> int:
|
||||
"""Return number of contacts in this chat."""
|
||||
return len(self.get_contacts())
|
||||
|
||||
def get_past_contacts(self) -> list[Contact]:
|
||||
"""Get past contacts for this chat."""
|
||||
past_contacts = self._rpc.get_past_chat_contacts(self.account.id, self.id)
|
||||
@@ -295,7 +303,7 @@ class Chat:
|
||||
f.flush()
|
||||
self._rpc.send_msg(self.account.id, self.id, {"viewtype": ViewType.VCARD, "file": f.name})
|
||||
|
||||
def place_outgoing_call(self, place_call_info: str) -> Message:
|
||||
def place_outgoing_call(self, place_call_info: str, has_video_initially: bool) -> Message:
|
||||
"""Starts an outgoing call."""
|
||||
msg_id = self._rpc.place_outgoing_call(self.account.id, self.id, place_call_info)
|
||||
msg_id = self._rpc.place_outgoing_call(self.account.id, self.id, place_call_info, has_video_initially)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
@@ -14,6 +14,7 @@ from typing import (
|
||||
|
||||
from ._utils import (
|
||||
AttrDict,
|
||||
_forever,
|
||||
parse_system_add_remove,
|
||||
parse_system_image_changed,
|
||||
parse_system_title_changed,
|
||||
@@ -91,19 +92,28 @@ class Client:
|
||||
|
||||
def run_forever(self) -> None:
|
||||
"""Process events forever."""
|
||||
self.run_until(lambda _: False)
|
||||
self.run_until(_forever)
|
||||
|
||||
def run_until(self, func: Callable[[AttrDict], bool]) -> AttrDict:
|
||||
"""Process events until the given callable evaluates to True.
|
||||
|
||||
The callable should accept an AttrDict object representing the
|
||||
last processed event. The event is returned when the callable
|
||||
evaluates to True.
|
||||
"""
|
||||
"""Start the event processing loop."""
|
||||
self.logger.debug("Listening to incoming events...")
|
||||
if self.is_configured():
|
||||
self.account.start_io()
|
||||
self._process_messages() # Process old messages.
|
||||
return self._process_events(until_func=func) # Loop over incoming events
|
||||
|
||||
def _process_events(
|
||||
self,
|
||||
until_func: Callable[[AttrDict], bool] = _forever,
|
||||
until_event: EventType = False,
|
||||
) -> AttrDict:
|
||||
"""Process events until the given callable evaluates to True,
|
||||
or until a certain event happens.
|
||||
|
||||
The until_func callable should accept an AttrDict object representing
|
||||
the last processed event. The event is returned when the callable
|
||||
evaluates to True.
|
||||
"""
|
||||
while True:
|
||||
event = self.account.wait_for_event()
|
||||
event["kind"] = EventType(event.kind)
|
||||
@@ -112,10 +122,13 @@ class Client:
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
self._process_messages()
|
||||
|
||||
stop = func(event)
|
||||
stop = until_func(event)
|
||||
if stop:
|
||||
return event
|
||||
|
||||
if event.kind == until_event:
|
||||
return event
|
||||
|
||||
def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None:
|
||||
for hook, evfilter in self._hooks.get(filter_type, []):
|
||||
if evfilter.filter(event):
|
||||
|
||||
@@ -80,6 +80,7 @@ class EventType(str, Enum):
|
||||
CONFIG_SYNCED = "ConfigSynced"
|
||||
WEBXDC_REALTIME_DATA = "WebxdcRealtimeData"
|
||||
WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED = "WebxdcRealtimeAdvertisementReceived"
|
||||
TRANSPORTS_MODIFIED = "TransportsModified"
|
||||
|
||||
|
||||
class ChatId(IntEnum):
|
||||
@@ -91,19 +92,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 +114,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()
|
||||
|
||||
@@ -44,6 +44,14 @@ class Message:
|
||||
read_receipts = self._rpc.get_message_read_receipts(self.account.id, self.id)
|
||||
return [AttrDict(read_receipt) for read_receipt in read_receipts]
|
||||
|
||||
def get_read_receipt_count(self) -> int:
|
||||
"""
|
||||
Returns count of read receipts on message.
|
||||
|
||||
This view count is meant as a feedback measure for the channel owner only.
|
||||
"""
|
||||
return self._rpc.get_message_read_receipt_count(self.account.id, self.id)
|
||||
|
||||
def get_reactions(self) -> Optional[AttrDict]:
|
||||
"""Get message reactions."""
|
||||
reactions = self._rpc.get_message_reactions(self.account.id, self.id)
|
||||
@@ -60,6 +68,10 @@ class Message:
|
||||
"""Mark the message as seen."""
|
||||
self._rpc.markseen_msgs(self.account.id, [self.id])
|
||||
|
||||
def exists(self) -> bool:
|
||||
"""Return True if the message exists."""
|
||||
return bool(self._rpc.get_existing_msg_ids(self.account.id, [self.id]))
|
||||
|
||||
def continue_autocrypt_key_transfer(self, setup_code: str) -> None:
|
||||
"""Continue the Autocrypt Setup Message key transfer.
|
||||
|
||||
@@ -93,6 +105,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."""
|
||||
|
||||
@@ -2,10 +2,16 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
import random
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import AsyncGenerator, Optional
|
||||
|
||||
import execnet
|
||||
import py
|
||||
import pytest
|
||||
|
||||
@@ -16,10 +22,22 @@ from .rpc import Rpc
|
||||
E2EE_INFO_MSGS = 1
|
||||
"""
|
||||
The number of info messages added to new e2ee chats.
|
||||
Currently this is "End-to-end encryption available".
|
||||
Currently this is "Messages are end-to-end encrypted."
|
||||
"""
|
||||
|
||||
|
||||
def pytest_report_header():
|
||||
for base in os.get_exec_path():
|
||||
fn = pathlib.Path(base).joinpath(base, "deltachat-rpc-server")
|
||||
if fn.exists():
|
||||
proc = subprocess.Popen([str(fn), "--version"], stderr=subprocess.PIPE)
|
||||
proc.wait()
|
||||
version = proc.stderr.read().decode().strip()
|
||||
return f"deltachat-rpc-server: {fn} [{version}]"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class ACFactory:
|
||||
"""Test account factory."""
|
||||
|
||||
@@ -36,16 +54,21 @@ class ACFactory:
|
||||
|
||||
def get_credentials(self) -> (str, str):
|
||||
"""Generate new credentials for chatmail account."""
|
||||
domain = os.getenv("CHATMAIL_DOMAIN")
|
||||
domain = os.environ["CHATMAIL_DOMAIN"]
|
||||
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
|
||||
return f"{username}@{domain}", f"{username}${username}"
|
||||
|
||||
def get_account_qr(self):
|
||||
"""Return "dcaccount:" QR code for testing chatmail relay."""
|
||||
domain = os.environ["CHATMAIL_DOMAIN"]
|
||||
return f"dcaccount:{domain}"
|
||||
|
||||
@futuremethod
|
||||
def new_configured_account(self):
|
||||
"""Create a new configured account."""
|
||||
account = self.get_unconfigured_account()
|
||||
domain = os.getenv("CHATMAIL_DOMAIN")
|
||||
yield account.add_transport_from_qr.future(f"dcaccount:{domain}")
|
||||
qr = self.get_account_qr()
|
||||
yield account.add_transport_from_qr.future(qr)
|
||||
|
||||
assert account.is_configured()
|
||||
return account
|
||||
@@ -77,6 +100,7 @@ class ACFactory:
|
||||
ac_clone = self.get_unconfigured_account()
|
||||
for transport in transports:
|
||||
ac_clone.add_or_update_transport(transport)
|
||||
ac_clone.bring_online()
|
||||
return ac_clone
|
||||
|
||||
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:
|
||||
@@ -135,9 +159,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
|
||||
@@ -175,13 +205,143 @@ def log():
|
||||
|
||||
class Printer:
|
||||
def section(self, msg: str) -> None:
|
||||
print()
|
||||
print("=" * 10, msg, "=" * 10)
|
||||
logging.info("\n%s %s %s", "=" * 10, msg, "=" * 10)
|
||||
|
||||
def step(self, msg: str) -> None:
|
||||
print("-" * 5, "step " + msg, "-" * 5)
|
||||
logging.info("%s step %s %s", "-" * 5, msg, "-" * 5)
|
||||
|
||||
def indent(self, msg: str) -> None:
|
||||
print(" " + msg)
|
||||
logging.info(" " + msg)
|
||||
|
||||
return Printer()
|
||||
|
||||
|
||||
#
|
||||
# support for testing against different deltachat-rpc-server/clients
|
||||
# installed into a temporary virtualenv and connected via 'execnet' channels
|
||||
#
|
||||
|
||||
|
||||
def find_path(venv, name):
|
||||
is_windows = platform.system() == "Windows"
|
||||
bin = venv / ("bin" if not is_windows else "Scripts")
|
||||
|
||||
tryadd = [""]
|
||||
if is_windows:
|
||||
tryadd += os.environ["PATHEXT"].split(os.pathsep)
|
||||
for ext in tryadd:
|
||||
p = bin.joinpath(name + ext)
|
||||
if p.exists():
|
||||
return str(p)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def get_core_python_env(tmp_path_factory):
|
||||
"""Return a factory to create virtualenv environments with rpc server/client packages
|
||||
installed.
|
||||
|
||||
The factory takes a version and returns a (python_path, rpc_server_path) tuple
|
||||
of the respective binaries in the virtualenv.
|
||||
"""
|
||||
|
||||
envs = {}
|
||||
|
||||
def get_versioned_venv(core_version):
|
||||
venv = envs.get(core_version)
|
||||
if not venv:
|
||||
venv = tmp_path_factory.mktemp(f"temp-{core_version}")
|
||||
subprocess.check_call([sys.executable, "-m", "venv", venv])
|
||||
|
||||
python = find_path(venv, "python")
|
||||
pkgs = [f"deltachat-rpc-server=={core_version}", f"deltachat-rpc-client=={core_version}", "pytest"]
|
||||
subprocess.check_call([python, "-m", "pip", "install"] + pkgs)
|
||||
|
||||
envs[core_version] = venv
|
||||
python = find_path(venv, "python")
|
||||
rpc_server_path = find_path(venv, "deltachat-rpc-server")
|
||||
logging.info(f"Paths:\npython={python}\nrpc_server={rpc_server_path}")
|
||||
return python, rpc_server_path
|
||||
|
||||
return get_versioned_venv
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def alice_and_remote_bob(tmp_path, acfactory, get_core_python_env):
|
||||
"""return local Alice account, a contact to bob, and a remote 'eval' function for bob.
|
||||
|
||||
The 'eval' function allows to remote-execute arbitrary expressions
|
||||
that can use the `bob` online account, and the `bob_contact_alice`.
|
||||
"""
|
||||
|
||||
def factory(core_version):
|
||||
python, rpc_server_path = get_core_python_env(core_version)
|
||||
gw = execnet.makegateway(f"popen//python={python}")
|
||||
|
||||
accounts_dir = str(tmp_path.joinpath("account1_venv1"))
|
||||
channel = gw.remote_exec(remote_bob_loop)
|
||||
cm = os.environ.get("CHATMAIL_DOMAIN")
|
||||
|
||||
# trigger getting an online account on bob's side
|
||||
channel.send((accounts_dir, str(rpc_server_path), cm))
|
||||
|
||||
# meanwhile get a local alice account
|
||||
alice = acfactory.get_online_account()
|
||||
channel.send(alice.self_contact.make_vcard())
|
||||
|
||||
# wait for bob to have started
|
||||
sysinfo = channel.receive()
|
||||
assert sysinfo == f"v{core_version}"
|
||||
bob_vcard = channel.receive()
|
||||
[alice_contact_bob] = alice.import_vcard(bob_vcard)
|
||||
|
||||
def eval(eval_str):
|
||||
channel.send(eval_str)
|
||||
return channel.receive()
|
||||
|
||||
return alice, alice_contact_bob, eval
|
||||
|
||||
return factory
|
||||
|
||||
|
||||
def remote_bob_loop(channel):
|
||||
# This function executes with versioned
|
||||
# deltachat-rpc-client/server packages
|
||||
# installed into the virtualenv.
|
||||
#
|
||||
# The "channel" argument is a send/receive pipe
|
||||
# to the process that runs the corresponding remote_exec(remote_bob_loop)
|
||||
|
||||
import os
|
||||
|
||||
from deltachat_rpc_client import DeltaChat, Rpc
|
||||
from deltachat_rpc_client.pytestplugin import ACFactory
|
||||
|
||||
accounts_dir, rpc_server_path, chatmail_domain = channel.receive()
|
||||
os.environ["CHATMAIL_DOMAIN"] = chatmail_domain
|
||||
|
||||
# older core versions don't support specifying rpc_server_path
|
||||
# so we can't just pass `rpc_server_path` argument to Rpc constructor
|
||||
basepath = os.path.dirname(rpc_server_path)
|
||||
os.environ["PATH"] = os.pathsep.join([basepath, os.environ["PATH"]])
|
||||
rpc = Rpc(accounts_dir=accounts_dir)
|
||||
|
||||
with rpc:
|
||||
dc = DeltaChat(rpc)
|
||||
channel.send(dc.rpc.get_system_info()["deltachat_core_version"])
|
||||
acfactory = ACFactory(dc)
|
||||
bob = acfactory.get_online_account()
|
||||
alice_vcard = channel.receive()
|
||||
[alice_contact] = bob.import_vcard(alice_vcard)
|
||||
ns = {"bob": bob, "bob_contact_alice": alice_contact}
|
||||
channel.send(bob.self_contact.make_vcard())
|
||||
|
||||
while 1:
|
||||
eval_str = channel.receive()
|
||||
res = eval(eval_str, ns)
|
||||
try:
|
||||
channel.send(res)
|
||||
except Exception:
|
||||
# some unserializable result
|
||||
channel.send(None)
|
||||
|
||||
@@ -9,7 +9,7 @@ import os
|
||||
import subprocess
|
||||
import sys
|
||||
from queue import Empty, Queue
|
||||
from threading import Event, Thread
|
||||
from threading import Thread
|
||||
from typing import Any, Iterator, Optional
|
||||
|
||||
|
||||
@@ -17,25 +17,6 @@ class JsonRpcError(Exception):
|
||||
"""JSON-RPC error."""
|
||||
|
||||
|
||||
class RpcFuture:
|
||||
"""RPC future waiting for RPC call result."""
|
||||
|
||||
def __init__(self, rpc: "Rpc", request_id: int, event: Event):
|
||||
self.rpc = rpc
|
||||
self.request_id = request_id
|
||||
self.event = event
|
||||
|
||||
def __call__(self):
|
||||
"""Wait for the future to return the result."""
|
||||
self.event.wait()
|
||||
response = self.rpc.request_results.pop(self.request_id)
|
||||
if "error" in response:
|
||||
raise JsonRpcError(response["error"])
|
||||
if "result" in response:
|
||||
return response["result"]
|
||||
return None
|
||||
|
||||
|
||||
class RpcMethod:
|
||||
"""RPC method."""
|
||||
|
||||
@@ -57,20 +38,31 @@ class RpcMethod:
|
||||
"params": args,
|
||||
"id": request_id,
|
||||
}
|
||||
event = Event()
|
||||
self.rpc.request_events[request_id] = event
|
||||
self.rpc.request_results[request_id] = queue = Queue()
|
||||
self.rpc.request_queue.put(request)
|
||||
|
||||
return RpcFuture(self.rpc, request_id, event)
|
||||
def rpc_future():
|
||||
"""Wait for the request to receive a result."""
|
||||
response = queue.get()
|
||||
if "error" in response:
|
||||
raise JsonRpcError(response["error"])
|
||||
return response.get("result", None)
|
||||
|
||||
return rpc_future
|
||||
|
||||
|
||||
class Rpc:
|
||||
"""RPC client."""
|
||||
|
||||
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
accounts_dir: Optional[str] = None,
|
||||
rpc_server_path="deltachat-rpc-server",
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize RPC client.
|
||||
|
||||
The given arguments will be passed to subprocess.Popen().
|
||||
The 'kwargs' arguments will be passed to subprocess.Popen().
|
||||
"""
|
||||
if accounts_dir:
|
||||
kwargs["env"] = {
|
||||
@@ -79,13 +71,12 @@ class Rpc:
|
||||
}
|
||||
|
||||
self._kwargs = kwargs
|
||||
self.rpc_server_path = rpc_server_path
|
||||
self.process: subprocess.Popen
|
||||
self.id_iterator: Iterator[int]
|
||||
self.event_queues: dict[int, Queue]
|
||||
# Map from request ID to `threading.Event`.
|
||||
self.request_events: dict[int, Event]
|
||||
# Map from request ID to the result.
|
||||
self.request_results: dict[int, Any]
|
||||
# Map from request ID to a Queue which provides a single result
|
||||
self.request_results: dict[int, Queue]
|
||||
self.request_queue: Queue[Any]
|
||||
self.closing: bool
|
||||
self.reader_thread: Thread
|
||||
@@ -93,28 +84,27 @@ class Rpc:
|
||||
self.events_thread: Thread
|
||||
|
||||
def start(self) -> None:
|
||||
"""Start RPC server subprocess."""
|
||||
"""Start RPC server subprocess and wait for successful initialization.
|
||||
|
||||
This method blocks until the RPC server responds to an initial
|
||||
health-check RPC call (get_system_info).
|
||||
If the server fails to start
|
||||
(e.g., due to an invalid accounts directory),
|
||||
a JsonRpcError is raised.
|
||||
"""
|
||||
popen_kwargs = {"stdin": subprocess.PIPE, "stdout": subprocess.PIPE, "stderr": subprocess.PIPE}
|
||||
if sys.version_info >= (3, 11):
|
||||
self.process = subprocess.Popen(
|
||||
"deltachat-rpc-server",
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
# Prevent subprocess from capturing SIGINT.
|
||||
process_group=0,
|
||||
**self._kwargs,
|
||||
)
|
||||
# Prevent subprocess from capturing SIGINT.
|
||||
popen_kwargs["process_group"] = 0
|
||||
else:
|
||||
self.process = subprocess.Popen(
|
||||
"deltachat-rpc-server",
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
# `process_group` is not supported before Python 3.11.
|
||||
preexec_fn=os.setpgrp, # noqa: PLW1509
|
||||
**self._kwargs,
|
||||
)
|
||||
# `process_group` is not supported before Python 3.11.
|
||||
popen_kwargs["preexec_fn"] = os.setpgrp # noqa: PLW1509
|
||||
|
||||
popen_kwargs.update(self._kwargs)
|
||||
self.process = subprocess.Popen(self.rpc_server_path, **popen_kwargs)
|
||||
|
||||
self.id_iterator = itertools.count(start=1)
|
||||
self.event_queues = {}
|
||||
self.request_events = {}
|
||||
self.request_results = {}
|
||||
self.request_queue = Queue()
|
||||
self.closing = False
|
||||
@@ -125,6 +115,22 @@ class Rpc:
|
||||
self.events_thread = Thread(target=self.events_loop)
|
||||
self.events_thread.start()
|
||||
|
||||
# Perform a health-check RPC call to ensure the server started
|
||||
# successfully and the accounts directory is usable.
|
||||
try:
|
||||
system_info = self.get_system_info()
|
||||
except (JsonRpcError, Exception) as e:
|
||||
# The reader_loop already saw EOF on stdout, so the process
|
||||
# has exited and stderr is available.
|
||||
stderr = self.process.stderr.read().decode(errors="replace").strip()
|
||||
if stderr:
|
||||
raise JsonRpcError(f"RPC server failed to start: {stderr}") from e
|
||||
raise JsonRpcError(f"RPC server startup check failed: {e}") from e
|
||||
logging.info(
|
||||
"RPC server ready. Core version: %s",
|
||||
system_info.get("deltachat_core_version", "unknown"),
|
||||
)
|
||||
|
||||
def close(self) -> None:
|
||||
"""Terminate RPC server process and wait until the reader loop finishes."""
|
||||
self.closing = True
|
||||
@@ -149,14 +155,16 @@ class Rpc:
|
||||
response = json.loads(line)
|
||||
if "id" in response:
|
||||
response_id = response["id"]
|
||||
event = self.request_events.pop(response_id)
|
||||
self.request_results[response_id] = response
|
||||
event.set()
|
||||
self.request_results.pop(response_id).put(response)
|
||||
else:
|
||||
logging.warning("Got a response without ID: %s", response)
|
||||
except Exception:
|
||||
# Log an exception if the reader loop dies.
|
||||
logging.exception("Exception in the reader loop")
|
||||
finally:
|
||||
# Unblock any pending requests when the server closes stdout.
|
||||
for _request_id, queue in self.request_results.items():
|
||||
queue.put({"error": {"code": -32000, "message": "RPC server closed"}})
|
||||
|
||||
def writer_loop(self) -> None:
|
||||
"""Writer loop ensuring only a single thread writes requests."""
|
||||
@@ -165,7 +173,6 @@ class Rpc:
|
||||
data = (json.dumps(request) + "\n").encode()
|
||||
self.process.stdin.write(data)
|
||||
self.process.stdin.flush()
|
||||
|
||||
except Exception:
|
||||
# Log an exception if the writer loop dies.
|
||||
logging.exception("Exception in the writer loop")
|
||||
@@ -179,15 +186,15 @@ class Rpc:
|
||||
def events_loop(self) -> None:
|
||||
"""Request new events and distributes them between queues."""
|
||||
try:
|
||||
while True:
|
||||
while events := self.get_next_event_batch():
|
||||
for event in events:
|
||||
account_id = event["contextId"]
|
||||
queue = self.get_queue(account_id)
|
||||
payload = event["event"]
|
||||
logging.debug("account_id=%d got an event %s", account_id, payload)
|
||||
queue.put(payload)
|
||||
if self.closing:
|
||||
return
|
||||
event = self.get_next_event()
|
||||
account_id = event["contextId"]
|
||||
queue = self.get_queue(account_id)
|
||||
event = event["event"]
|
||||
logging.debug("account_id=%d got an event %s", account_id, event)
|
||||
queue.put(event)
|
||||
except Exception:
|
||||
# Log an exception if the event loop dies.
|
||||
logging.exception("Exception in the event loop")
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import imaplib
|
||||
import io
|
||||
import logging
|
||||
import pathlib
|
||||
import ssl
|
||||
from contextlib import contextmanager
|
||||
@@ -45,13 +46,13 @@ class DirectImap:
|
||||
try:
|
||||
self.conn.logout()
|
||||
except (OSError, imaplib.IMAP4.abort):
|
||||
print("Could not logout direct_imap conn")
|
||||
logging.warning("Could not logout direct_imap conn")
|
||||
|
||||
def create_folder(self, foldername):
|
||||
try:
|
||||
self.conn.folder.create(foldername)
|
||||
except errors.MailboxFolderCreateError as e:
|
||||
print("Can't create", foldername, "probably it already exists:", str(e))
|
||||
logging.warning(f"Cannot create '{foldername}', probably it already exists: {str(e)}")
|
||||
|
||||
def select_folder(self, foldername: str) -> tuple:
|
||||
assert not self._idling
|
||||
@@ -85,17 +86,17 @@ 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()
|
||||
if messages:
|
||||
res = self.conn.flag(messages, MailMessageFlags.SEEN, True)
|
||||
print("marked seen:", messages, res)
|
||||
logging.info(f"Marked seen: {messages} {res}")
|
||||
|
||||
def get_unread_cnt(self) -> int:
|
||||
return len(self.get_unread_messages())
|
||||
@@ -173,7 +174,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 +181,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 +193,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."""
|
||||
|
||||
@@ -10,15 +10,15 @@ def test_calls(acfactory) -> None:
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
|
||||
outgoing_call_message = alice_chat_bob.place_outgoing_call(place_call_info)
|
||||
outgoing_call_message = alice_chat_bob.place_outgoing_call(place_call_info, has_video_initially=True)
|
||||
assert outgoing_call_message.get_call_info().state.kind == "Alerting"
|
||||
|
||||
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
|
||||
assert incoming_call_event.place_call_info == place_call_info
|
||||
assert not incoming_call_event.has_video # Cannot be parsed as SDP, so false by default
|
||||
assert incoming_call_event.has_video
|
||||
incoming_call_message = Message(bob, incoming_call_event.msg_id)
|
||||
assert incoming_call_message.get_call_info().state.kind == "Alerting"
|
||||
assert not incoming_call_message.get_call_info().has_video
|
||||
assert incoming_call_message.get_call_info().has_video
|
||||
|
||||
incoming_call_message.accept_incoming_call(accept_call_info)
|
||||
assert incoming_call_message.get_call_info().sdp_offer == place_call_info
|
||||
@@ -41,46 +41,38 @@ def test_video_call(acfactory) -> None:
|
||||
#
|
||||
# `s=` cannot be empty according to RFC 3264,
|
||||
# so it is more clear as `s=-`.
|
||||
place_call_info = """v=0\r
|
||||
o=alice 2890844526 2890844526 IN IP6 2001:db8::3\r
|
||||
s=-\r
|
||||
c=IN IP6 2001:db8::3\r
|
||||
t=0 0\r
|
||||
a=group:BUNDLE foo bar\r
|
||||
\r
|
||||
m=audio 10000 RTP/AVP 0 8 97\r
|
||||
b=AS:200\r
|
||||
a=mid:foo\r
|
||||
a=rtcp-mux\r
|
||||
a=rtpmap:0 PCMU/8000\r
|
||||
a=rtpmap:8 PCMA/8000\r
|
||||
a=rtpmap:97 iLBC/8000\r
|
||||
a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r
|
||||
\r
|
||||
m=video 10002 RTP/AVP 31 32\r
|
||||
b=AS:1000\r
|
||||
a=mid:bar\r
|
||||
a=rtcp-mux\r
|
||||
a=rtpmap:31 H261/90000\r
|
||||
a=rtpmap:32 MPV/90000\r
|
||||
a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r
|
||||
"""
|
||||
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.place_outgoing_call(place_call_info)
|
||||
alice_chat_bob.place_outgoing_call("offer", has_video_initially=True)
|
||||
|
||||
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
|
||||
assert incoming_call_event.place_call_info == place_call_info
|
||||
assert incoming_call_event.place_call_info == "offer"
|
||||
assert incoming_call_event.has_video
|
||||
|
||||
incoming_call_message = Message(bob, incoming_call_event.msg_id)
|
||||
assert incoming_call_message.get_call_info().has_video
|
||||
|
||||
|
||||
def test_audio_call(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.place_outgoing_call("offer", has_video_initially=False)
|
||||
|
||||
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
|
||||
assert incoming_call_event.place_call_info == "offer"
|
||||
assert not incoming_call_event.has_video
|
||||
|
||||
incoming_call_message = Message(bob, incoming_call_event.msg_id)
|
||||
assert not incoming_call_message.get_call_info().has_video
|
||||
|
||||
|
||||
def test_ice_servers(acfactory) -> None:
|
||||
alice = acfactory.get_online_account()
|
||||
|
||||
@@ -92,7 +84,7 @@ def test_no_contact_request_call(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
alice_chat_bob.place_outgoing_call("offer")
|
||||
alice_chat_bob.place_outgoing_call("offer", has_video_initially=True)
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
# Notification for "Hello!" message should arrive
|
||||
@@ -107,3 +99,48 @@ def test_no_contact_request_call(acfactory) -> None:
|
||||
msg = bob.get_message_by_id(event.msg_id)
|
||||
if msg.get_snapshot().text == "Hello!":
|
||||
break
|
||||
|
||||
|
||||
def test_who_can_call_me_nobody(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
# Bob sets "who can call me" to "nobody" (2)
|
||||
bob.set_config("who_can_call_me", "2")
|
||||
|
||||
# Bob even accepts Alice in advance so the chat does not appear as contact request.
|
||||
bob.create_chat(alice)
|
||||
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
alice_chat_bob.place_outgoing_call("offer", has_video_initially=True)
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
# Notification for "Hello!" message should arrive
|
||||
# without the call ringing.
|
||||
while True:
|
||||
event = bob.wait_for_event()
|
||||
|
||||
# There should be no incoming call notification.
|
||||
assert event.kind != EventType.INCOMING_CALL
|
||||
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
msg = bob.get_message_by_id(event.msg_id)
|
||||
if msg.get_snapshot().text == "Hello!":
|
||||
break
|
||||
|
||||
|
||||
def test_who_can_call_me_everybody(acfactory) -> None:
|
||||
"""Test that if "who can call me" setting is set to "everybody", calls arrive even in contact request chats."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
# Bob sets "who can call me" to "nobody" (0)
|
||||
bob.set_config("who_can_call_me", "0")
|
||||
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
alice_chat_bob.place_outgoing_call("offer", has_video_initially=True)
|
||||
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
|
||||
|
||||
incoming_call_message = Message(bob, incoming_call_event.msg_id)
|
||||
|
||||
# Even with the call arriving, the chat is still in the contact request mode.
|
||||
incoming_chat = incoming_call_message.get_snapshot().chat
|
||||
assert incoming_chat.get_basic_snapshot().is_contact_request
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from deltachat_rpc_client import Account, EventType, const
|
||||
@@ -129,7 +127,7 @@ def test_download_on_demand(acfactory: ACFactory) -> None:
|
||||
msg.get_snapshot().chat.accept()
|
||||
bob.get_chat_by_id(chat_id).send_message(
|
||||
"Hello World, this message is bigger than 5 bytes",
|
||||
html=base64.b64encode(os.urandom(300000)).decode("utf-8"),
|
||||
file="../test-data/image/screenshot.jpg",
|
||||
)
|
||||
|
||||
message = alice.wait_for_incoming_msg()
|
||||
|
||||
57
deltachat-rpc-client/tests/test_cross_core.py
Normal file
57
deltachat-rpc-client/tests/test_cross_core.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import DeltaChat, Rpc
|
||||
|
||||
|
||||
def test_install_venv_and_use_other_core(tmp_path, get_core_python_env):
|
||||
python, rpc_server_path = get_core_python_env("2.24.0")
|
||||
subprocess.check_call([python, "-m", "pip", "install", "deltachat-rpc-server==2.24.0"])
|
||||
rpc = Rpc(accounts_dir=tmp_path.joinpath("accounts"), rpc_server_path=rpc_server_path)
|
||||
|
||||
with rpc:
|
||||
dc = DeltaChat(rpc)
|
||||
assert dc.rpc.get_system_info()["deltachat_core_version"] == "v2.24.0"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", ["2.24.0"])
|
||||
def test_qr_setup_contact(alice_and_remote_bob, version) -> None:
|
||||
"""Test other-core Bob profile can do securejoin with Alice on current core."""
|
||||
alice, alice_contact_bob, remote_eval = alice_and_remote_bob(version)
|
||||
|
||||
qr_code = alice.get_qr_code()
|
||||
remote_eval(f"bob.secure_join({qr_code!r})")
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
|
||||
# Test that Alice verified Bob's profile.
|
||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||
assert alice_contact_bob_snapshot.is_verified
|
||||
|
||||
remote_eval("bob.wait_for_securejoin_joiner_success()")
|
||||
|
||||
# Test that Bob verified Alice's profile.
|
||||
assert remote_eval("bob_contact_alice.get_snapshot().is_verified")
|
||||
|
||||
|
||||
def test_send_and_receive_message(alice_and_remote_bob) -> None:
|
||||
"""Test other-core Bob profile can send a message to Alice on current core."""
|
||||
alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.20.0")
|
||||
|
||||
remote_eval("bob_contact_alice.create_chat().send_text('hello')")
|
||||
|
||||
msg = alice.wait_for_incoming_msg()
|
||||
assert msg.get_snapshot().text == "hello"
|
||||
|
||||
|
||||
def test_second_device(acfactory, alice_and_remote_bob) -> None:
|
||||
"""Test setting up current version as a second device for old version."""
|
||||
_alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.20.0")
|
||||
|
||||
remote_eval("locals().setdefault('future', bob._rpc.provide_backup.future(bob.id))")
|
||||
qr = remote_eval("bob._rpc.get_backup_qr(bob.id)")
|
||||
new_account = acfactory.get_unconfigured_account()
|
||||
new_account._rpc.get_backup(new_account.id, qr)
|
||||
remote_eval("locals()['future']()")
|
||||
|
||||
assert new_account.get_config("addr") == remote_eval("bob.get_config('addr')")
|
||||
212
deltachat-rpc-client/tests/test_folders.py
Normal file
212
deltachat-rpc-client/tests/test_folders.py
Normal file
@@ -0,0 +1,212 @@
|
||||
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, direct_imap):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac2_direct_imap = direct_imap(ac2)
|
||||
ac2_direct_imap.create_folder("DeltaChat")
|
||||
ac2.set_config("mvbox_move", "1")
|
||||
ac2.bring_online()
|
||||
|
||||
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_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_direct_imap = direct_imap(ac2)
|
||||
ac2_direct_imap.create_folder("DeltaChat")
|
||||
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_move_works_on_self_sent(acfactory, direct_imap):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
# Create and enable movebox.
|
||||
ac1_direct_imap = direct_imap(ac1)
|
||||
ac1_direct_imap.create_folder("DeltaChat")
|
||||
ac1.set_config("mvbox_move", "1")
|
||||
ac1.set_config("bcc_self", "1")
|
||||
ac1.bring_online()
|
||||
|
||||
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_direct_imap = direct_imap(ac2)
|
||||
ac2_direct_imap.create_folder("DeltaChat")
|
||||
ac2.set_config("mvbox_move", "1")
|
||||
ac2.set_config("delete_server_after", "0")
|
||||
ac2.set_config("sync_msgs", "0") # Do not send a sync message when accepting a contact request.
|
||||
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_direct_imap = direct_imap(ac)
|
||||
ac_direct_imap.create_folder("DeltaChat")
|
||||
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_trash_multiple_messages(acfactory, direct_imap, log):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac2.stop_io()
|
||||
|
||||
ac2.set_config("delete_server_after", "0")
|
||||
ac2.set_config("sync_msgs", "0")
|
||||
|
||||
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)
|
||||
|
||||
log.section("ac2: deleting all messages except second")
|
||||
assert len(to_delete) == len(texts) - 1
|
||||
ac2.delete_messages(to_delete)
|
||||
|
||||
log.section("ac2: test that only one message is left")
|
||||
ac2_direct_imap = direct_imap(ac2)
|
||||
while 1:
|
||||
ac2.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
|
||||
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
|
||||
@@ -24,6 +24,13 @@ def path_to_webxdc(request):
|
||||
return str(p)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def path_to_large_webxdc(request):
|
||||
p = request.path.parent.parent.parent.joinpath("test-data/webxdc/realtime-check.xdc")
|
||||
assert p.exists()
|
||||
return str(p)
|
||||
|
||||
|
||||
def log(msg):
|
||||
logging.info(msg)
|
||||
|
||||
@@ -84,7 +91,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 +101,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 +109,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")
|
||||
@@ -227,3 +234,29 @@ def test_advertisement_after_chatting(acfactory, path_to_webxdc):
|
||||
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||
event = ac1.wait_for_event(EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED)
|
||||
assert event.msg_id == ac1_webxdc_msg.id
|
||||
|
||||
|
||||
def test_realtime_large_webxdc(acfactory, path_to_large_webxdc):
|
||||
"""Tests initializing realtime channel on a large webxdc.
|
||||
|
||||
This is a regression test for a bug that existed in version 2.42.0.
|
||||
Large webxdc is split into pre- and post- message,
|
||||
and this previously resulted in failure to initialize realtime.
|
||||
"""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||
|
||||
ac2.create_chat(ac1)
|
||||
ac1_ac2_chat = ac1.create_chat(ac2)
|
||||
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="realtime check", file=path_to_large_webxdc)
|
||||
|
||||
# Receive pre-message.
|
||||
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
|
||||
|
||||
# Receive post-message.
|
||||
ac2_webxdc_msg = ac2.wait_for_msg(EventType.MSGS_CHANGED)
|
||||
|
||||
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||
event = ac1.wait_for_event(EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED)
|
||||
assert event.msg_id == ac1_webxdc_msg.id
|
||||
|
||||
@@ -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()
|
||||
|
||||
386
deltachat-rpc-client/tests/test_multitransport.py
Normal file
386
deltachat-rpc-client/tests/test_multitransport.py
Normal file
@@ -0,0 +1,386 @@
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import EventType
|
||||
from deltachat_rpc_client.const import DownloadState
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
def test_add_second_address(acfactory) -> None:
|
||||
account = acfactory.new_configured_account()
|
||||
assert len(account.list_transports()) == 1
|
||||
|
||||
# When the first transport is created,
|
||||
# mvbox_move and only_fetch_mvbox should be disabled.
|
||||
assert account.get_config("mvbox_move") == "0"
|
||||
assert account.get_config("only_fetch_mvbox") == "0"
|
||||
assert account.get_config("show_emails") == "2"
|
||||
|
||||
qr = acfactory.get_account_qr()
|
||||
account.add_transport_from_qr(qr)
|
||||
assert len(account.list_transports()) == 2
|
||||
|
||||
account.add_transport_from_qr(qr)
|
||||
assert len(account.list_transports()) == 3
|
||||
|
||||
first_addr = account.list_transports()[0]["addr"]
|
||||
second_addr = account.list_transports()[1]["addr"]
|
||||
|
||||
# Cannot delete the first address.
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.delete_transport(first_addr)
|
||||
|
||||
account.delete_transport(second_addr)
|
||||
assert len(account.list_transports()) == 2
|
||||
|
||||
# Enabling mvbox_move or only_fetch_mvbox
|
||||
# is not allowed when multi-transport is enabled.
|
||||
for option in ["mvbox_move", "only_fetch_mvbox"]:
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.set_config(option, "1")
|
||||
|
||||
# show_emails does not matter for multi-relay, can be set to anything
|
||||
account.set_config("show_emails", "0")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", ["mvbox_move", "only_fetch_mvbox"])
|
||||
def test_no_second_transport_with_mvbox(acfactory, key) -> None:
|
||||
"""Test that second transport cannot be configured if mvbox is used."""
|
||||
account = acfactory.new_configured_account()
|
||||
assert len(account.list_transports()) == 1
|
||||
|
||||
assert account.get_config("mvbox_move") == "0"
|
||||
assert account.get_config("only_fetch_mvbox") == "0"
|
||||
|
||||
qr = acfactory.get_account_qr()
|
||||
account.set_config(key, "1")
|
||||
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
|
||||
def test_second_transport_without_classic_emails(acfactory) -> None:
|
||||
"""Test that second transport can be configured if classic emails are not fetched."""
|
||||
account = acfactory.new_configured_account()
|
||||
assert len(account.list_transports()) == 1
|
||||
|
||||
assert account.get_config("show_emails") == "2"
|
||||
|
||||
qr = acfactory.get_account_qr()
|
||||
account.set_config("show_emails", "0")
|
||||
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
|
||||
def test_change_address(acfactory) -> None:
|
||||
"""Test Alice configuring a second transport and setting it as a primary one."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("configured_addr")
|
||||
bob.create_chat(alice)
|
||||
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
msg1 = bob.wait_for_incoming_msg().get_snapshot()
|
||||
sender_addr1 = msg1.sender.get_snapshot().address
|
||||
|
||||
alice.stop_io()
|
||||
old_alice_addr = alice.get_config("configured_addr")
|
||||
alice_vcard = alice.self_contact.make_vcard()
|
||||
assert old_alice_addr in alice_vcard
|
||||
qr = acfactory.get_account_qr()
|
||||
alice.add_transport_from_qr(qr)
|
||||
new_alice_addr = alice.list_transports()[1]["addr"]
|
||||
with pytest.raises(JsonRpcError):
|
||||
# Cannot use the address that is not
|
||||
# configured for any transport.
|
||||
alice.set_config("configured_addr", bob_addr)
|
||||
|
||||
# Load old address so it is cached.
|
||||
assert alice.get_config("configured_addr") == old_alice_addr
|
||||
alice.set_config("configured_addr", new_alice_addr)
|
||||
# Make sure that setting `configured_addr` invalidated the cache.
|
||||
assert alice.get_config("configured_addr") == new_alice_addr
|
||||
|
||||
alice_vcard = alice.self_contact.make_vcard()
|
||||
assert old_alice_addr not in alice_vcard
|
||||
assert new_alice_addr in alice_vcard
|
||||
with pytest.raises(JsonRpcError):
|
||||
alice.delete_transport(new_alice_addr)
|
||||
alice.start_io()
|
||||
|
||||
alice_chat_bob.send_text("Hello again!")
|
||||
|
||||
msg2 = bob.wait_for_incoming_msg().get_snapshot()
|
||||
sender_addr2 = msg2.sender.get_snapshot().address
|
||||
|
||||
assert msg1.sender == msg2.sender
|
||||
assert sender_addr1 != sender_addr2
|
||||
assert sender_addr1 == old_alice_addr
|
||||
assert sender_addr2 == new_alice_addr
|
||||
|
||||
|
||||
def test_download_on_demand(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice.set_config("download_limit", "1")
|
||||
|
||||
alice.stop_io()
|
||||
qr = acfactory.get_account_qr()
|
||||
alice.add_transport_from_qr(qr)
|
||||
alice.start_io()
|
||||
|
||||
alice.create_chat(bob)
|
||||
chat_bob_alice = bob.create_chat(alice)
|
||||
chat_bob_alice.send_message(file="../test-data/image/screenshot.jpg")
|
||||
msg = alice.wait_for_incoming_msg()
|
||||
snapshot = msg.get_snapshot()
|
||||
assert snapshot.download_state == DownloadState.AVAILABLE
|
||||
chat_id = snapshot.chat_id
|
||||
# Actually the message isn't available yet. Wait somehow for the post-message to arrive.
|
||||
chat_bob_alice.send_message("Now you can download my previous message")
|
||||
alice.wait_for_incoming_msg()
|
||||
alice._rpc.download_full_message(alice.id, msg.id)
|
||||
for dstate in [DownloadState.IN_PROGRESS, DownloadState.DONE]:
|
||||
event = alice.wait_for_event(EventType.MSGS_CHANGED)
|
||||
assert event.chat_id == chat_id
|
||||
assert event.msg_id == msg.id
|
||||
assert msg.get_snapshot().download_state == dstate
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_chatmail", ["0", "1"])
|
||||
def test_mvbox_move_first_transport(acfactory, is_chatmail) -> None:
|
||||
"""Test that mvbox_move is disabled by default even for non-chatmail accounts.
|
||||
Disabling mvbox_move is required to be able to setup a second transport.
|
||||
"""
|
||||
account = acfactory.get_unconfigured_account()
|
||||
|
||||
account.set_config("fix_is_chatmail", "1")
|
||||
account.set_config("is_chatmail", is_chatmail)
|
||||
|
||||
# The default value when the setting is unset is "1".
|
||||
# This is not changed for compatibility with old databases
|
||||
# imported from backups.
|
||||
assert account.get_config("mvbox_move") == "1"
|
||||
|
||||
qr = acfactory.get_account_qr()
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
# Once the first transport is set up,
|
||||
# mvbox_move is disabled.
|
||||
assert account.get_config("mvbox_move") == "0"
|
||||
assert account.get_config("is_chatmail") == is_chatmail
|
||||
|
||||
|
||||
def test_reconfigure_transport(acfactory) -> None:
|
||||
"""Test that reconfiguring the transport works
|
||||
even if settings not supported for multi-transport
|
||||
like mvbox_move are enabled."""
|
||||
account = acfactory.get_online_account()
|
||||
account.set_config("mvbox_move", "1")
|
||||
|
||||
[transport] = account.list_transports()
|
||||
account.add_or_update_transport(transport)
|
||||
|
||||
# Reconfiguring the transport should not reset
|
||||
# the settings as if when configuring the first transport.
|
||||
assert account.get_config("mvbox_move") == "1"
|
||||
|
||||
|
||||
def test_transport_synchronization(acfactory, log) -> None:
|
||||
"""Test synchronization of transports between devices."""
|
||||
|
||||
def wait_for_io_started(ac):
|
||||
while True:
|
||||
ev = ac.wait_for_event(EventType.INFO)
|
||||
if "scheduler is running" in ev.msg:
|
||||
return
|
||||
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1_clone = ac1.clone()
|
||||
ac1_clone.bring_online()
|
||||
|
||||
qr = acfactory.get_account_qr()
|
||||
|
||||
ac1.add_transport_from_qr(qr)
|
||||
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
wait_for_io_started(ac1_clone)
|
||||
assert len(ac1.list_transports()) == 2
|
||||
assert len(ac1_clone.list_transports()) == 2
|
||||
|
||||
ac1_clone.add_transport_from_qr(qr)
|
||||
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
wait_for_io_started(ac1)
|
||||
assert len(ac1.list_transports()) == 3
|
||||
assert len(ac1_clone.list_transports()) == 3
|
||||
|
||||
log.section("ac1 clone removes second transport")
|
||||
[transport1, transport2, transport3] = ac1_clone.list_transports()
|
||||
addr3 = transport3["addr"]
|
||||
ac1_clone.delete_transport(transport2["addr"])
|
||||
|
||||
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
wait_for_io_started(ac1)
|
||||
[transport1, transport3] = ac1.list_transports()
|
||||
|
||||
log.section("ac1 changes the primary transport")
|
||||
ac1.set_config("configured_addr", transport3["addr"])
|
||||
|
||||
# One event for updated `add_timestamp` of the new primary transport,
|
||||
# one event for the `configured_addr` update.
|
||||
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
[transport1, transport3] = ac1_clone.list_transports()
|
||||
assert ac1_clone.get_config("configured_addr") == addr3
|
||||
|
||||
log.section("ac1 removes the first transport")
|
||||
ac1.delete_transport(transport1["addr"])
|
||||
|
||||
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
wait_for_io_started(ac1_clone)
|
||||
[transport3] = ac1_clone.list_transports()
|
||||
assert transport3["addr"] == addr3
|
||||
assert ac1_clone.get_config("configured_addr") == addr3
|
||||
|
||||
ac2_chat = ac2.create_chat(ac1)
|
||||
ac2_chat.send_text("Hello!")
|
||||
|
||||
assert ac1.wait_for_incoming_msg().get_snapshot().text == "Hello!"
|
||||
assert ac1_clone.wait_for_incoming_msg().get_snapshot().text == "Hello!"
|
||||
|
||||
|
||||
def test_transport_sync_new_as_primary(acfactory, log) -> None:
|
||||
"""Test synchronization of new transport as primary between devices."""
|
||||
ac1, bob = acfactory.get_online_accounts(2)
|
||||
ac1_clone = ac1.clone()
|
||||
ac1_clone.bring_online()
|
||||
|
||||
qr = acfactory.get_account_qr()
|
||||
|
||||
ac1.add_transport_from_qr(qr)
|
||||
ac1_transports = ac1.list_transports()
|
||||
assert len(ac1_transports) == 2
|
||||
[transport1, transport2] = ac1_transports
|
||||
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
assert len(ac1_clone.list_transports()) == 2
|
||||
assert ac1_clone.get_config("configured_addr") == transport1["addr"]
|
||||
|
||||
log.section("ac1 changes the primary transport")
|
||||
ac1.set_config("configured_addr", transport2["addr"])
|
||||
|
||||
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
assert ac1_clone.get_config("configured_addr") == transport2["addr"]
|
||||
|
||||
log.section("ac1_clone receives a message via the new primary transport")
|
||||
ac1_chat = ac1.create_chat(bob)
|
||||
ac1_chat.send_text("Hello!")
|
||||
bob_chat_id = bob.wait_for_incoming_msg_event().chat_id
|
||||
bob_chat = bob.get_chat_by_id(bob_chat_id)
|
||||
bob_chat.accept()
|
||||
bob_chat.send_text("hello back")
|
||||
assert ac1_clone.wait_for_incoming_msg().get_snapshot().text == "hello back"
|
||||
|
||||
|
||||
def test_recognize_self_address(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_chat = bob.create_chat(alice)
|
||||
|
||||
qr = acfactory.get_account_qr()
|
||||
alice.add_transport_from_qr(qr)
|
||||
|
||||
new_alice_addr = alice.list_transports()[1]["addr"]
|
||||
alice.set_config("configured_addr", new_alice_addr)
|
||||
|
||||
bob_chat.send_text("Hello!")
|
||||
msg = alice.wait_for_incoming_msg().get_snapshot()
|
||||
assert msg.chat == alice.create_chat(bob)
|
||||
|
||||
|
||||
def test_transport_limit(acfactory) -> None:
|
||||
"""Test transports limit."""
|
||||
account = acfactory.get_online_account()
|
||||
qr = acfactory.get_account_qr()
|
||||
|
||||
limit = 5
|
||||
|
||||
for _ in range(1, limit):
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
assert len(account.list_transports()) == limit
|
||||
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
second_addr = account.list_transports()[1]["addr"]
|
||||
account.delete_transport(second_addr)
|
||||
|
||||
# test that adding a transport after deleting one works again
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
|
||||
def test_message_info_imap_urls(acfactory) -> None:
|
||||
"""Test that message info contains IMAP URLs of where the message was received."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
qr = acfactory.get_account_qr()
|
||||
for i in range(3):
|
||||
alice.add_transport_from_qr(qr)
|
||||
# Wait for all transports to go IDLE after adding each one.
|
||||
for _ in range(i + 1):
|
||||
alice.bring_online()
|
||||
|
||||
# Enable multi-device mode so messages are not deleted immediately.
|
||||
alice.set_config("bcc_self", "1")
|
||||
|
||||
# Bob creates chat, learning about Alice's currently selected transport.
|
||||
# This is where he will send the message.
|
||||
bob_chat = bob.create_chat(alice)
|
||||
|
||||
# Alice switches to another transport and removes the rest of the transports.
|
||||
new_alice_addr = alice.list_transports()[1]["addr"]
|
||||
alice.set_config("configured_addr", new_alice_addr)
|
||||
removed_addrs = []
|
||||
for transport in alice.list_transports():
|
||||
if transport["addr"] != new_alice_addr:
|
||||
alice.delete_transport(transport["addr"])
|
||||
removed_addrs.append(transport["addr"])
|
||||
alice.stop_io()
|
||||
alice.start_io()
|
||||
|
||||
bob_chat.send_text("Hello!")
|
||||
|
||||
msg = alice.wait_for_incoming_msg()
|
||||
msg_info = msg.get_info()
|
||||
assert new_alice_addr in msg_info
|
||||
for removed_addr in removed_addrs:
|
||||
assert removed_addr not in msg_info
|
||||
assert f"{new_alice_addr}/INBOX" in msg_info
|
||||
|
||||
|
||||
def test_remove_primary_transport(acfactory) -> None:
|
||||
"""Test that after removing the primary relay, Alice can still receive messages."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
qr = acfactory.get_account_qr()
|
||||
|
||||
alice.add_transport_from_qr(qr)
|
||||
alice.bring_online()
|
||||
|
||||
bob_chat = bob.create_chat(alice)
|
||||
alice.create_chat(bob)
|
||||
|
||||
# Alice changes the transport.
|
||||
[transport1, transport2] = alice.list_transports()
|
||||
alice.set_config("configured_addr", transport2["addr"])
|
||||
|
||||
bob_chat.send_text("Hello!")
|
||||
msg1 = alice.wait_for_incoming_msg().get_snapshot()
|
||||
assert msg1.text == "Hello!"
|
||||
|
||||
# Alice deletes the first transport.
|
||||
alice.delete_transport(transport1["addr"])
|
||||
alice.stop_io()
|
||||
alice.start_io()
|
||||
|
||||
bob_chat.send_text("Hello again!")
|
||||
msg2 = alice.wait_for_incoming_msg().get_snapshot()
|
||||
assert msg2.text == "Hello again!"
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -85,7 +86,7 @@ def test_qr_securejoin(acfactory):
|
||||
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"))
|
||||
|
||||
# Test that Bob verified Alice's profile.
|
||||
@@ -109,6 +110,148 @@ def test_qr_securejoin(acfactory):
|
||||
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()
|
||||
|
||||
encrypted_msg = chat_msgs.pop(0).get_snapshot()
|
||||
assert encrypted_msg.text == "Messages are end-to-end encrypted."
|
||||
assert encrypted_msg.is_info
|
||||
|
||||
if please_wait_info_msg:
|
||||
first_msg = chat_msgs.pop(0).get_snapshot()
|
||||
assert "invited you to join this channel" in first_msg.text
|
||||
assert first_msg.is_info
|
||||
|
||||
if inviter_side:
|
||||
member_added_msg = chat_msgs.pop(0).get_snapshot()
|
||||
assert member_added_msg.text == f"Member {contact_snapshot.display_name} added."
|
||||
assert member_added_msg.info_contact_id == contact_snapshot.id
|
||||
else:
|
||||
if chat_msgs[0].get_snapshot().text == "You joined the channel.":
|
||||
member_added_msg = chat_msgs.pop(0).get_snapshot()
|
||||
else:
|
||||
member_added_msg = chat_msgs.pop(1).get_snapshot()
|
||||
assert member_added_msg.text == "You joined the channel."
|
||||
assert member_added_msg.is_info
|
||||
|
||||
hello_msg = chat_msgs.pop(0).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) == 0
|
||||
|
||||
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)
|
||||
@@ -117,7 +260,7 @@ 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
|
||||
@@ -161,8 +304,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"
|
||||
|
||||
@@ -173,8 +315,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!"
|
||||
|
||||
@@ -249,7 +390,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)
|
||||
@@ -262,18 +403,17 @@ 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)
|
||||
@@ -309,7 +449,7 @@ 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":
|
||||
break
|
||||
|
||||
@@ -330,7 +470,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||
# 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":
|
||||
break
|
||||
|
||||
@@ -367,7 +507,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
|
||||
|
||||
@@ -392,7 +532,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")
|
||||
@@ -406,7 +546,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
|
||||
@@ -438,7 +578,7 @@ def test_gossip_verification(acfactory) -> None:
|
||||
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
|
||||
|
||||
@@ -454,7 +594,7 @@ def test_gossip_verification(acfactory) -> None:
|
||||
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
|
||||
|
||||
@@ -482,7 +622,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()
|
||||
|
||||
@@ -519,7 +659,7 @@ 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 len(ac2_chat.get_contacts()) == 3
|
||||
@@ -541,7 +681,7 @@ 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"))
|
||||
bob_chat.leave()
|
||||
|
||||
@@ -561,6 +701,6 @@ def test_withdraw_securejoin_qr(acfactory):
|
||||
event = alice.wait_for_event()
|
||||
if (
|
||||
event.kind == EventType.WARNING
|
||||
and "Ignoring vg-request-with-auth message because of invalid auth code." in event.msg
|
||||
and "Ignoring RequestWithAuth message because of invalid auth code." in event.msg
|
||||
):
|
||||
break
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.23.0"
|
||||
version = "2.46.0-dev"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.23.0"
|
||||
"version": "2.46.0-dev"
|
||||
}
|
||||
|
||||
@@ -24,6 +24,14 @@ use yerpc::{RpcClient, RpcSession};
|
||||
|
||||
#[tokio::main(flavor = "multi_thread")]
|
||||
async fn main() {
|
||||
// Logs from `log` crate and traces from `tracing` crate
|
||||
// are configurable with `RUST_LOG` environment variable
|
||||
// and go to stderr to avoid interfering with JSON-RPC using stdout.
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(EnvFilter::from_default_env())
|
||||
.with_writer(std::io::stderr)
|
||||
.init();
|
||||
|
||||
let r = main_impl().await;
|
||||
// From tokio documentation:
|
||||
// "For technical reasons, stdin is implemented by using an ordinary blocking read on a separate
|
||||
@@ -43,7 +51,7 @@ async fn main_impl() -> Result<()> {
|
||||
if let Some(arg) = args.next() {
|
||||
return Err(anyhow!("Unrecognized argument {arg:?}"));
|
||||
}
|
||||
eprintln!("{}", &*DC_VERSION_STR);
|
||||
eprintln!("{DC_VERSION_STR}");
|
||||
return Ok(());
|
||||
} else if first_arg.to_str() == Some("--openrpc") {
|
||||
if let Some(arg) = args.next() {
|
||||
@@ -64,14 +72,6 @@ async fn main_impl() -> Result<()> {
|
||||
#[cfg(target_family = "unix")]
|
||||
let mut sigterm = signal_unix::signal(signal_unix::SignalKind::terminate())?;
|
||||
|
||||
// Logs from `log` crate and traces from `tracing` crate
|
||||
// are configurable with `RUST_LOG` environment variable
|
||||
// and go to stderr to avoid interfering with JSON-RPC using stdout.
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(EnvFilter::from_default_env())
|
||||
.with_writer(std::io::stderr)
|
||||
.init();
|
||||
|
||||
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "accounts".to_string());
|
||||
log::info!("Starting with accounts directory `{path}`.");
|
||||
let writable = true;
|
||||
|
||||
10
deny.toml
10
deny.toml
@@ -12,6 +12,11 @@ ignore = [
|
||||
|
||||
# Unmaintained paste
|
||||
"RUSTSEC-2024-0436",
|
||||
|
||||
# Unmaintained rustls-pemfile
|
||||
# It is a transitive dependency of iroh 0.35.0,
|
||||
# this should be fixed by upgrading to iroh 1.0 once it is released.
|
||||
"RUSTSEC-2025-0134",
|
||||
]
|
||||
|
||||
[bans]
|
||||
@@ -26,11 +31,10 @@ skip = [
|
||||
{ name = "derive_more", version = "1.0.0" },
|
||||
{ name = "event-listener", version = "2.5.3" },
|
||||
{ name = "getrandom", version = "0.2.12" },
|
||||
{ name = "hashbrown", version = "0.14.5" },
|
||||
{ name = "heck", version = "0.4.1" },
|
||||
{ name = "http", version = "0.2.12" },
|
||||
{ name = "linux-raw-sys", version = "0.4.14" },
|
||||
{ name = "lru", version = "0.12.3" },
|
||||
{ name = "lru", version = "0.12.5" },
|
||||
{ name = "netlink-packet-route", version = "0.17.1" },
|
||||
{ name = "nom", version = "7.1.3" },
|
||||
{ name = "rand_chacha", version = "0.3.1" },
|
||||
@@ -38,6 +42,7 @@ skip = [
|
||||
{ name = "rand", version = "0.8.5" },
|
||||
{ name = "rustix", version = "0.38.44" },
|
||||
{ name = "serdect", version = "0.2.0" },
|
||||
{ name = "socket2", version = "0.5.9" },
|
||||
{ name = "spin", version = "0.9.8" },
|
||||
{ name = "strum_macros", version = "0.26.2" },
|
||||
{ name = "strum", version = "0.26.2" },
|
||||
@@ -62,7 +67,6 @@ skip = [
|
||||
{ name = "windows_x86_64_gnu" },
|
||||
{ name = "windows_x86_64_gnullvm" },
|
||||
{ name = "windows_x86_64_msvc" },
|
||||
{ name = "zerocopy", version = "0.7.32" },
|
||||
]
|
||||
|
||||
|
||||
|
||||
18
flake.lock
generated
18
flake.lock
generated
@@ -47,11 +47,11 @@
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1747291057,
|
||||
"narHash": "sha256-9Wir6aLJAeJKqdoQUiwfKdBn7SyNXTJGRSscRyVOo2Y=",
|
||||
"lastModified": 1763361733,
|
||||
"narHash": "sha256-ka7dpwH3HIXCyD2wl5F7cPLeRbqZoY2ullALsvxdPt8=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "76ffc1b7b3ec8078fe01794628b6abff35cbda8f",
|
||||
"rev": "6c8d48e3b0ae371b19ac1485744687b788e80193",
|
||||
"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": {
|
||||
@@ -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": {
|
||||
|
||||
24
flake.nix
24
flake.nix
@@ -1,5 +1,5 @@
|
||||
{
|
||||
description = "Delta Chat core";
|
||||
description = "Chatmail core";
|
||||
inputs = {
|
||||
fenix.url = "github:nix-community/fenix";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
@@ -14,7 +14,15 @@
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
inherit (pkgs.stdenv) isDarwin;
|
||||
fenixPkgs = fenix.packages.${system};
|
||||
naersk' = pkgs.callPackage naersk { };
|
||||
fenixToolchain = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
fenixPkgs.stable.rust-std
|
||||
];
|
||||
naersk' = pkgs.callPackage naersk {
|
||||
cargo = fenixToolchain;
|
||||
rustc = fenixToolchain;
|
||||
};
|
||||
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
|
||||
androidSdk = android.sdk.${system} (sdkPkgs:
|
||||
builtins.attrValues {
|
||||
@@ -34,7 +42,6 @@
|
||||
./Cargo.lock
|
||||
./Cargo.toml
|
||||
./CMakeLists.txt
|
||||
./CONTRIBUTING.md
|
||||
./deltachat_derive
|
||||
./deltachat-contact-tools
|
||||
./deltachat-ffi
|
||||
@@ -471,6 +478,12 @@
|
||||
};
|
||||
|
||||
libdeltachat =
|
||||
let
|
||||
rustPlatform = (pkgs.makeRustPlatform {
|
||||
cargo = fenixToolchain;
|
||||
rustc = fenixToolchain;
|
||||
});
|
||||
in
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "libdeltachat";
|
||||
version = manifest.version;
|
||||
@@ -480,8 +493,9 @@
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
pkgs.cmake
|
||||
pkgs.rustPlatform.cargoSetupHook
|
||||
pkgs.cargo
|
||||
rustPlatform.cargoSetupHook
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
];
|
||||
|
||||
postInstall = ''
|
||||
|
||||
@@ -14,6 +14,7 @@ def datadir():
|
||||
return None
|
||||
|
||||
|
||||
@pytest.mark.skip("The test is flaky in CI and crashes the interpreter as of 2025-11-12")
|
||||
def test_echo_quit_plugin(acfactory, lp):
|
||||
lp.sec("creating one echo_and_quit bot")
|
||||
botproc = acfactory.run_bot_process(echo_and_quit)
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=45", "wheel", "cffi>=1.0.0", "pkgconfig"]
|
||||
requires = ["setuptools>=77", "wheel", "cffi>=1.0.0", "pkgconfig"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "2.23.0"
|
||||
version = "2.46.0-dev"
|
||||
license = "MPL-2.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.8"
|
||||
requires-python = ">=3.10"
|
||||
authors = [
|
||||
{ name = "holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors" },
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Topic :: Communications :: Chat",
|
||||
"Topic :: Communications :: Email",
|
||||
@@ -23,7 +23,6 @@ classifiers = [
|
||||
dependencies = [
|
||||
"cffi>=1.0.0",
|
||||
"imap-tools",
|
||||
"importlib_metadata;python_version<'3.8'",
|
||||
"pluggy",
|
||||
"requests",
|
||||
]
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import sys
|
||||
import time
|
||||
|
||||
import deltachat as dc
|
||||
@@ -63,56 +62,6 @@ class TestGroupStressTests:
|
||||
# Message should be encrypted because keys of other members are gossiped
|
||||
assert msg.is_encrypted()
|
||||
|
||||
def test_synchronize_member_list_on_group_rejoin(self, acfactory, lp):
|
||||
"""
|
||||
Test that user recreates group member list when it joins the group again.
|
||||
ac1 creates a group with two other accounts: ac2 and ac3
|
||||
Then it removes ac2, removes ac3 and adds ac2 back.
|
||||
ac2 did not see that ac3 is removed, so it should rebuild member list from scratch.
|
||||
"""
|
||||
lp.sec("setting up accounts, accepted with each other")
|
||||
accounts = acfactory.get_online_accounts(3)
|
||||
acfactory.introduce_each_other(accounts)
|
||||
ac1, ac2, ac3 = accounts
|
||||
|
||||
lp.sec("ac1: creating group chat with 2 other members")
|
||||
chat = ac1.create_group_chat("title1", contacts=[ac2, ac3])
|
||||
assert not chat.is_promoted()
|
||||
|
||||
lp.sec("ac1: send message to new group chat")
|
||||
msg = chat.send_text("hello")
|
||||
assert chat.is_promoted() and msg.is_encrypted()
|
||||
|
||||
assert chat.num_contacts() == 3
|
||||
|
||||
lp.sec("checking that the chat arrived correctly")
|
||||
for ac in accounts[1:]:
|
||||
msg = ac._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
print("chat is", msg.chat)
|
||||
assert msg.chat.num_contacts() == 3
|
||||
|
||||
lp.sec("ac1: removing ac2")
|
||||
chat.remove_contact(ac2)
|
||||
|
||||
lp.sec("ac2: wait for a message about removal from the chat")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
lp.sec("ac1: removing ac3")
|
||||
chat.remove_contact(ac3)
|
||||
|
||||
lp.sec("ac1: adding ac2 back")
|
||||
# Group is promoted, message is sent automatically
|
||||
assert chat.is_promoted()
|
||||
chat.add_contact(ac2)
|
||||
|
||||
lp.sec("ac2: check that ac3 is removed")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
assert chat.num_contacts() == 2
|
||||
assert msg.chat.num_contacts() == 2
|
||||
acfactory.dump_imap_summary(sys.stdout)
|
||||
|
||||
|
||||
def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import os
|
||||
import queue
|
||||
import sys
|
||||
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
|
||||
from deltachat.tracker import ImexTracker
|
||||
from deltachat.testplugin import E2EE_INFO_MSGS
|
||||
|
||||
|
||||
@@ -222,177 +220,6 @@ def test_webxdc_huge_update(acfactory, data, lp):
|
||||
assert update["payload"] == payload
|
||||
|
||||
|
||||
def test_webxdc_download_on_demand(acfactory, data, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
acfactory.introduce_each_other([ac1, ac2])
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
msg1 = Message.new_empty(ac1, "webxdc")
|
||||
msg1.set_text("message1")
|
||||
msg1.set_file(data.get_path("webxdc/minimal.xdc"))
|
||||
msg1 = chat.send_msg(msg1)
|
||||
assert msg1.is_webxdc()
|
||||
assert msg1.filename
|
||||
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.is_webxdc()
|
||||
|
||||
lp.sec("ac2 sets download limit")
|
||||
ac2.set_config("download_limit", "100")
|
||||
assert msg1.send_status_update({"payload": base64.b64encode(os.urandom(300000))}, "some test data")
|
||||
ac2_update = ac2._evtracker.wait_next_incoming_message()
|
||||
assert ac2_update.download_state == dc.const.DC_DOWNLOAD_AVAILABLE
|
||||
assert not msg2.get_status_updates()
|
||||
|
||||
ac2_update.download_full()
|
||||
ac2._evtracker.get_matching("DC_EVENT_WEBXDC_STATUS_UPDATE")
|
||||
assert msg2.get_status_updates()
|
||||
|
||||
# Get a event notifying that the message disappeared from the chat.
|
||||
msgs_changed_event = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
assert msgs_changed_event.data1 == msg2.chat.id
|
||||
assert msgs_changed_event.data2 == 0
|
||||
|
||||
|
||||
def test_enable_mvbox_move(acfactory, lp):
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
|
||||
lp.sec("ac2: start without mvbox thread")
|
||||
ac2 = acfactory.new_online_configuring_account(mvbox_move=False)
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
lp.sec("ac2: configuring mvbox")
|
||||
ac2.set_config("mvbox_move", "1")
|
||||
|
||||
lp.sec("ac1: send message and wait for ac2 to receive it")
|
||||
acfactory.get_accepted_chat(ac1, ac2).send_text("message1")
|
||||
assert ac2._evtracker.wait_next_incoming_message().text == "message1"
|
||||
|
||||
|
||||
def test_mvbox_thread_and_trash(acfactory, lp):
|
||||
lp.sec("ac1: start with mvbox thread")
|
||||
ac1 = acfactory.new_online_configuring_account(mvbox_move=True)
|
||||
|
||||
lp.sec("ac2: start without a mvbox thread")
|
||||
ac2 = acfactory.new_online_configuring_account(mvbox_move=False)
|
||||
|
||||
lp.sec("ac2 and ac1: waiting for configuration")
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
lp.sec("ac1: create trash")
|
||||
ac1.direct_imap.create_folder("Trash")
|
||||
ac1.set_config("scan_all_folders_debounce_secs", "0")
|
||||
ac1.stop_io()
|
||||
ac1.start_io()
|
||||
|
||||
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_trash_folder") != "Trash":
|
||||
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 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 = acfactory.new_online_configuring_account()
|
||||
ac2 = acfactory.new_online_configuring_account(mvbox_move=True)
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
# Create INBOX.DeltaChat folder and make sure
|
||||
# it is detected by full folder scan.
|
||||
ac2.direct_imap.create_folder("INBOX.DeltaChat")
|
||||
ac2.stop_io()
|
||||
ac2.start_io()
|
||||
ac2._evtracker.get_info_contains("Found folders:") # Wait until the end of folder scan.
|
||||
|
||||
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.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._evtracker.wait_next_incoming_message()
|
||||
assert ac2_msg2.text == "Message 2"
|
||||
|
||||
# Stop and start I/O to trigger folder scan.
|
||||
ac2.stop_io()
|
||||
ac2.start_io()
|
||||
ac2._evtracker.get_info_contains("Found folders:") # Wait until the end of folder scan.
|
||||
|
||||
# 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_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()
|
||||
|
||||
ac1.direct_imap.select_folder("DeltaChat")
|
||||
# Sync messages may also be sent during the configuration.
|
||||
mvbox_msg_cnt = len(ac1.direct_imap.get_all_messages())
|
||||
|
||||
ac1.set_config("displayname", "Alice")
|
||||
ac1._evtracker.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||
ac1.set_config("displayname", "Bob")
|
||||
ac1._evtracker.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||
ac1.direct_imap.select_folder("Inbox")
|
||||
assert len(ac1.direct_imap.get_all_messages()) == 0
|
||||
ac1.direct_imap.select_folder("DeltaChat")
|
||||
assert len(ac1.direct_imap.get_all_messages()) == mvbox_msg_cnt + 2
|
||||
|
||||
|
||||
def test_forward_messages(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = ac1.create_chat(ac2)
|
||||
@@ -508,7 +335,7 @@ def test_long_group_name(acfactory, lp):
|
||||
|
||||
|
||||
def test_send_self_message(acfactory, lp):
|
||||
ac1 = acfactory.new_online_configuring_account(mvbox_move=True, bcc_self=True)
|
||||
ac1 = acfactory.new_online_configuring_account(bcc_self=True)
|
||||
acfactory.bring_accounts_online()
|
||||
lp.sec("ac1: create self chat")
|
||||
chat = ac1.get_self_contact().create_chat()
|
||||
@@ -607,39 +434,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")
|
||||
@@ -674,36 +468,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)
|
||||
|
||||
@@ -730,7 +494,7 @@ def test_reply_privately(acfactory):
|
||||
|
||||
|
||||
def test_mdn_asymmetric(acfactory, lp):
|
||||
ac1 = acfactory.new_online_configuring_account(mvbox_move=True)
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac2 = acfactory.new_online_configuring_account()
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
@@ -759,20 +523,14 @@ def test_mdn_asymmetric(acfactory, lp):
|
||||
ac2.mark_seen_messages([msg])
|
||||
|
||||
lp.sec("ac1: waiting for incoming activity")
|
||||
# MDN should be moved even though MDNs are already disabled
|
||||
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
|
||||
assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS
|
||||
|
||||
# Wait for the message to be marked as seen on IMAP.
|
||||
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder DeltaChat as seen.")
|
||||
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
|
||||
|
||||
# MDN is received even though MDNs are already disabled
|
||||
assert msg_out.is_out_mdn_received()
|
||||
|
||||
ac1.direct_imap.select_config_folder("mvbox")
|
||||
assert len(list(ac1.direct_imap.conn.fetch(AND(seen=True)))) == 1
|
||||
|
||||
|
||||
def test_send_receive_encrypt(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
@@ -853,140 +611,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, 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_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("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 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")
|
||||
lp.sec("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._evtracker.wait_idle_inbox_ready()
|
||||
|
||||
fresh_msgs = list(ac1.get_fresh_messages())
|
||||
msg = fresh_msgs[0]
|
||||
chat_msgs = msg.chat.get_messages()
|
||||
assert len(chat_msgs) == 1
|
||||
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 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()
|
||||
msg2 = ac1._evtracker.wait_next_messages_changed()
|
||||
|
||||
assert msg2.text == "subj – message in Drafts received later"
|
||||
assert len(msg.chat.get_messages()) == 2
|
||||
|
||||
|
||||
def test_bot(acfactory, lp):
|
||||
"""Test that bot messages can be identified as such"""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
@@ -1129,86 +753,6 @@ def test_send_and_receive_image(acfactory, lp, data):
|
||||
assert m == msg_in
|
||||
|
||||
|
||||
def test_import_export_online_all(acfactory, tmp_path, data, lp):
|
||||
(ac1, some1) = acfactory.get_online_accounts(2)
|
||||
|
||||
lp.sec("create some chat content")
|
||||
some1_addr = some1.get_config("addr")
|
||||
chat1 = ac1.create_contact(some1).create_chat()
|
||||
chat1.send_text("msg1")
|
||||
assert len(ac1.get_contacts()) == 1
|
||||
|
||||
original_image_path = data.get_path("d.png")
|
||||
chat1.send_image(original_image_path)
|
||||
|
||||
# Add another 100KB file that ensures that the progress is smooth enough
|
||||
path = tmp_path / "attachment.txt"
|
||||
with path.open("w") as file:
|
||||
file.truncate(100000)
|
||||
chat1.send_file(str(path))
|
||||
|
||||
def assert_account_is_proper(ac):
|
||||
contacts = ac.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == some1_addr
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 3 + E2EE_INFO_MSGS
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert messages[1 + E2EE_INFO_MSGS].filemime == "image/png"
|
||||
assert os.stat(messages[1 + E2EE_INFO_MSGS].filename).st_size == os.stat(original_image_path).st_size
|
||||
ac.set_config("displayname", "new displayname")
|
||||
assert ac.get_config("displayname") == "new displayname"
|
||||
|
||||
assert_account_is_proper(ac1)
|
||||
|
||||
backupdir = tmp_path / "backup"
|
||||
backupdir.mkdir()
|
||||
|
||||
lp.sec(f"export all to {backupdir}")
|
||||
with ac1.temp_plugin(ImexTracker()) as imex_tracker:
|
||||
ac1.stop_io()
|
||||
ac1.imex(str(backupdir), dc.const.DC_IMEX_EXPORT_BACKUP)
|
||||
|
||||
# check progress events for export
|
||||
assert imex_tracker.wait_progress(1, progress_upper_limit=249)
|
||||
assert imex_tracker.wait_progress(250, progress_upper_limit=499)
|
||||
assert imex_tracker.wait_progress(500, progress_upper_limit=749)
|
||||
assert imex_tracker.wait_progress(750, progress_upper_limit=999)
|
||||
|
||||
paths = imex_tracker.wait_finish()
|
||||
assert len(paths) == 1
|
||||
path = paths[0]
|
||||
assert os.path.exists(path)
|
||||
ac1.start_io()
|
||||
|
||||
lp.sec("get fresh empty account")
|
||||
ac2 = acfactory.get_unconfigured_account()
|
||||
|
||||
lp.sec("get latest backup file")
|
||||
path2 = ac2.get_latest_backupfile(str(backupdir))
|
||||
assert path2 == path
|
||||
|
||||
lp.sec("import backup and check it's proper")
|
||||
with ac2.temp_plugin(ImexTracker()) as imex_tracker:
|
||||
ac2.import_all(path)
|
||||
|
||||
# check progress events for import
|
||||
assert imex_tracker.wait_progress(1, progress_upper_limit=249)
|
||||
assert imex_tracker.wait_progress(1000)
|
||||
|
||||
assert_account_is_proper(ac1)
|
||||
assert_account_is_proper(ac2)
|
||||
|
||||
lp.sec(f"Second-time export all to {backupdir}")
|
||||
ac1.stop_io()
|
||||
path2 = ac1.export_all(str(backupdir))
|
||||
assert os.path.exists(path2)
|
||||
assert path2 != path
|
||||
assert ac2.get_latest_backupfile(str(backupdir)) == path2
|
||||
|
||||
|
||||
def test_qr_email_capitalization(acfactory, lp):
|
||||
"""Regression test for a bug
|
||||
that resulted in failure to propagate verification
|
||||
@@ -1388,7 +932,6 @@ def test_set_get_group_image(acfactory, data, lp):
|
||||
|
||||
def test_connectivity(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("scan_all_folders_debounce_secs", "0")
|
||||
|
||||
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTED)
|
||||
|
||||
@@ -1509,9 +1052,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
|
||||
@@ -1522,38 +1071,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)
|
||||
@@ -1586,55 +1103,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)
|
||||
|
||||
@@ -1673,16 +1141,17 @@ def test_configure_error_msgs_invalid_server(acfactory):
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_CONFIGURE_PROGRESS")
|
||||
if ev.data1 == 0:
|
||||
break
|
||||
err_lower = ev.data2.lower()
|
||||
# Can't connect so it probably should say something about "internet"
|
||||
# again, should not repeat itself
|
||||
# If this fails then probably `e.msg.to_lowercase().contains("could not resolve")`
|
||||
# in configure.rs returned false because the error message was changed
|
||||
# (i.e. did not contain "could not resolve" anymore)
|
||||
assert (ev.data2.count("internet") + ev.data2.count("network")) == 1
|
||||
assert (err_lower.count("internet") + err_lower.count("network")) == 1
|
||||
# Should mention that it can't connect:
|
||||
assert ev.data2.count("connect") == 1
|
||||
assert err_lower.count("connect") == 1
|
||||
# The users do not know what "configuration" is
|
||||
assert "configuration" not in ev.data2.lower()
|
||||
assert "configuration" not in err_lower
|
||||
|
||||
|
||||
def test_status(acfactory):
|
||||
@@ -1758,71 +1227,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
|
||||
(
|
||||
"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, 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()
|
||||
@@ -258,9 +258,6 @@ class TestOfflineChat:
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(dc.const.DC_STR_FILE, "xyz %1$s")
|
||||
ac1._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(dc.const.DC_STR_CONTACT_NOT_VERIFIED, "xyz %2$s")
|
||||
ac1._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(500, "xyz %1$s")
|
||||
ac1._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
@@ -561,6 +558,12 @@ class TestOfflineChat:
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
|
||||
|
||||
@pytest.mark.skip(
|
||||
reason="We didn't find a way to correctly reset an account after a failed import attempt "
|
||||
"while simultaneously making sure "
|
||||
"that the password of an encrypted account survives a failed import attempt. "
|
||||
"Since passphrases are not really supported anymore, we decided to just disable the test.",
|
||||
)
|
||||
def test_import_encrypted_bak_into_encrypted_acct(self, acfactory, tmp_path):
|
||||
"""
|
||||
Test that account passphrase isn't lost if backup failed to be imported.
|
||||
|
||||
@@ -111,7 +111,7 @@ def test_dc_close_events(acfactory):
|
||||
register_global_plugin(ShutdownPlugin())
|
||||
assert hasattr(ac1, "_dc_context")
|
||||
ac1.shutdown()
|
||||
shutdowns.get(timeout=2)
|
||||
shutdowns.get()
|
||||
|
||||
|
||||
def test_wrong_db(tmp_path):
|
||||
@@ -221,7 +221,7 @@ def test_logged_ac_process_ffi_failure(acfactory):
|
||||
|
||||
# cause any event eg contact added/changed
|
||||
ac1.create_contact("something@example.org")
|
||||
res = cap.get(timeout=10)
|
||||
res = cap.get()
|
||||
assert "ac_process_ffi_event" in res
|
||||
assert "ZeroDivisionError" in res
|
||||
assert "Traceback" in res
|
||||
|
||||
@@ -46,7 +46,7 @@ deps =
|
||||
commands =
|
||||
ruff format --diff setup.py src/deltachat examples/ tests/
|
||||
ruff check src/deltachat tests/ examples/
|
||||
rst-lint --encoding 'utf-8' README.rst
|
||||
rst-lint README.rst
|
||||
|
||||
[testenv:mypy]
|
||||
deps =
|
||||
|
||||
@@ -1 +1 @@
|
||||
2025-11-01
|
||||
2026-03-14
|
||||
@@ -26,10 +26,10 @@ and an own build machine.
|
||||
i.e. `deltachat-rpc-client` and `deltachat-rpc-server`.
|
||||
|
||||
- `remote_tests_python.sh` rsyncs to a build machine and runs
|
||||
`run-python-test.sh` remotely on the build machine.
|
||||
JSON-RPC Python tests remotely on the build machine.
|
||||
|
||||
- `remote_tests_rust.sh` rsyncs to the build machine and runs
|
||||
`run-rust-test.sh` remotely on the build machine.
|
||||
Rust tests remotely on the build machine.
|
||||
|
||||
- `run-doxygen.sh` generates C-docs which are then uploaded to https://c.delta.chat/
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ set -euo pipefail
|
||||
#
|
||||
# Avoid using rustup here as it depends on reading /proc/self/exe and
|
||||
# has problems running under QEMU.
|
||||
RUST_VERSION=1.91.0
|
||||
RUST_VERSION=1.94.0
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# Update package cache without changing the lockfile.
|
||||
cargo update --dry-run
|
||||
|
||||
cargo deny --workspace --all-features check -D warnings
|
||||
cargo deny --workspace --all-features --locked check -D warnings
|
||||
|
||||
@@ -1,45 +1,32 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BUILD_ID=${1:?specify build ID}
|
||||
|
||||
SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
|
||||
BUILDDIR=ci_builds/$BUILD_ID
|
||||
set -x
|
||||
if ! test -v SSHTARGET; then
|
||||
echo >&2 SSHTARGET is not set
|
||||
exit 1
|
||||
fi
|
||||
BUILDDIR=ci_builds/chatmailcore
|
||||
|
||||
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
|
||||
|
||||
set -xe
|
||||
|
||||
ssh -oBatchMode=yes -oStrictHostKeyChecking=no $SSHTARGET mkdir -p "$BUILDDIR"
|
||||
git ls-files >.rsynclist
|
||||
# we seem to need .git for setuptools_scm versioning
|
||||
find .git >>.rsynclist
|
||||
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
|
||||
|
||||
set +x
|
||||
rsync -az --delete --mkpath --files-from=<(git ls-files) ./ "$SSHTARGET:$BUILDDIR"
|
||||
|
||||
echo "--- Running Python tests remotely"
|
||||
|
||||
ssh $SSHTARGET <<_HERE
|
||||
ssh -oBatchMode=yes -- "$SSHTARGET" <<_HERE
|
||||
set +x -e
|
||||
|
||||
# make sure all processes exit when ssh dies
|
||||
shopt -s huponexit
|
||||
|
||||
export RUSTC_WRAPPER=\`which sccache\`
|
||||
export RUSTC_WRAPPER=\`command -v sccache\`
|
||||
cd $BUILDDIR
|
||||
export TARGET=release
|
||||
export CHATMAIL_DOMAIN=$CHATMAIL_DOMAIN
|
||||
|
||||
#we rely on tox/virtualenv being available in the host
|
||||
#rm -rf virtualenv venv
|
||||
#virtualenv -q -p python3.7 venv
|
||||
#source venv/bin/activate
|
||||
#pip install -q tox virtualenv
|
||||
scripts/make-rpc-testenv.sh
|
||||
. venv/bin/activate
|
||||
|
||||
set -x
|
||||
which python
|
||||
source \$HOME/venv/bin/activate
|
||||
which python
|
||||
|
||||
bash scripts/run-python-test.sh
|
||||
cd deltachat-rpc-client
|
||||
pytest -n6 $@
|
||||
_HERE
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BUILD_ID=${1:?specify build ID}
|
||||
|
||||
SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
|
||||
BUILDDIR=ci_builds/$BUILD_ID
|
||||
|
||||
set -e
|
||||
if ! test -v SSHTARGET; then
|
||||
echo >&2 SSHTARGET is not set
|
||||
exit 1
|
||||
fi
|
||||
BUILDDIR=ci_builds/chatmailcore
|
||||
|
||||
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
|
||||
|
||||
ssh -oBatchMode=yes -oStrictHostKeyChecking=no $SSHTARGET mkdir -p "$BUILDDIR"
|
||||
git ls-files >.rsynclist
|
||||
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
|
||||
rsync -az --delete --mkpath --files-from=<(git ls-files) ./ "$SSHTARGET:$BUILDDIR"
|
||||
|
||||
echo "--- Running Rust tests remotely"
|
||||
|
||||
ssh $SSHTARGET <<_HERE
|
||||
ssh -oBatchMode=yes -- "$SSHTARGET" <<_HERE
|
||||
set +x -e
|
||||
# make sure all processes exit when ssh dies
|
||||
shopt -s huponexit
|
||||
export RUSTC_WRAPPER=\`which sccache\`
|
||||
export RUSTC_WRAPPER=\`command -v sccache\`
|
||||
cd $BUILDDIR
|
||||
export TARGET=x86_64-unknown-linux-gnu
|
||||
export RUSTC_WRAPPER=sccache
|
||||
|
||||
bash scripts/run-rust-test.sh
|
||||
cargo nextest run
|
||||
_HERE
|
||||
|
||||
|
||||
@@ -31,6 +31,6 @@ unset CHATMAIL_DOMAIN
|
||||
|
||||
# Try to build wheels for a range of interpreters, but don't fail if they are not available.
|
||||
# E.g. musllinux_1_1 does not have PyPy interpreters as of 2022-07-10
|
||||
tox --workdir "$TOXWORKDIR" -e py38,py39,py310,py311,py312,py313,pypy38,pypy39,pypy310 --skip-missing-interpreters true
|
||||
tox --workdir "$TOXWORKDIR" -e py310,py311,py312,py313,pypy310 --skip-missing-interpreters true
|
||||
|
||||
auditwheel repair "$TOXWORKDIR"/wheelhouse/deltachat* -w "$TOXWORKDIR/wheelhouse"
|
||||
|
||||
@@ -100,7 +100,7 @@ def main():
|
||||
|
||||
today = datetime.date.today().isoformat()
|
||||
|
||||
if "alpha" not in newversion:
|
||||
if not newversion.endswith("-dev"):
|
||||
found = False
|
||||
for line in Path("CHANGELOG.md").open():
|
||||
if line == f"## [{newversion}] - {today}\n":
|
||||
|
||||
@@ -6,11 +6,11 @@ set -euo pipefail
|
||||
export TZ=UTC
|
||||
|
||||
# Provider database revision.
|
||||
REV=d041136c19a48b493823b46d472f12b9ee94ae80
|
||||
REV=996c4bc82be5a7404f70b185ff062da33bfa98d9
|
||||
|
||||
CORE_ROOT="$PWD"
|
||||
TMP="$(mktemp -d)"
|
||||
git clone --filter=blob:none https://github.com/deltachat/provider-db.git "$TMP"
|
||||
git clone --filter=blob:none https://github.com/chatmail/provider-db.git "$TMP"
|
||||
cd "$TMP"
|
||||
git checkout "$REV"
|
||||
DATE=$(git show -s --format=%cs)
|
||||
|
||||
172
src/accounts.rs
172
src/accounts.rs
@@ -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,16 +45,32 @@ 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 {
|
||||
/// Loads or creates an accounts folder at the given `dir`.
|
||||
pub async fn new(dir: PathBuf, writable: bool) -> Result<Self> {
|
||||
if writable && !dir.exists() {
|
||||
Accounts::create(&dir).await?;
|
||||
if writable {
|
||||
Self::ensure_accounts_dir(&dir).await?;
|
||||
}
|
||||
let events = Events::new();
|
||||
Accounts::open(events, dir, writable).await
|
||||
}
|
||||
|
||||
Accounts::open(dir, writable).await
|
||||
/// Loads or creates an accounts folder at the given `dir`.
|
||||
/// Uses an existing events channel.
|
||||
pub async fn new_with_events(dir: PathBuf, writable: bool, events: Events) -> Result<Self> {
|
||||
if writable {
|
||||
Self::ensure_accounts_dir(&dir).await?;
|
||||
}
|
||||
Accounts::open(events, dir, writable).await
|
||||
}
|
||||
|
||||
/// Get the ID used to log events.
|
||||
@@ -61,27 +81,33 @@ impl Accounts {
|
||||
0
|
||||
}
|
||||
|
||||
/// Creates a new default structure.
|
||||
async fn create(dir: &Path) -> Result<()> {
|
||||
fs::create_dir_all(dir)
|
||||
.await
|
||||
.context("failed to create folder")?;
|
||||
|
||||
Config::new(dir).await?;
|
||||
|
||||
/// Ensures the accounts directory and config file exist.
|
||||
/// Creates them if the directory doesn't exist, or if it exists but is empty.
|
||||
/// Errors if the directory exists with files but no config.
|
||||
async fn ensure_accounts_dir(dir: &Path) -> Result<()> {
|
||||
if !dir.exists() {
|
||||
fs::create_dir_all(dir)
|
||||
.await
|
||||
.context("Failed to create folder")?;
|
||||
Config::new(dir).await?;
|
||||
} else if !dir.join(CONFIG_NAME).exists() {
|
||||
let mut rd = fs::read_dir(dir).await?;
|
||||
ensure!(rd.next_entry().await?.is_none(), "{dir:?} is not empty");
|
||||
Config::new(dir).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Opens an existing accounts structure. Will error if the folder doesn't exist,
|
||||
/// no account exists and no config exists.
|
||||
async fn open(dir: PathBuf, writable: bool) -> Result<Self> {
|
||||
async fn open(events: Events, dir: PathBuf, writable: bool) -> Result<Self> {
|
||||
ensure!(dir.exists(), "directory does not exist");
|
||||
|
||||
let config_file = dir.join(CONFIG_NAME);
|
||||
ensure!(config_file.exists(), "{config_file:?} does not exist");
|
||||
|
||||
let config = Config::from_file(config_file, writable).await?;
|
||||
let events = Events::new();
|
||||
|
||||
let stockstrings = StockStrings::new();
|
||||
let push_subscriber = PushSubscriber::new();
|
||||
let accounts = config
|
||||
@@ -96,6 +122,7 @@ impl Accounts {
|
||||
events,
|
||||
stockstrings,
|
||||
push_subscriber,
|
||||
background_fetch_interrupt_sender: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -352,6 +379,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 {
|
||||
@@ -360,6 +392,11 @@ impl Accounts {
|
||||
"Starting background fetch for {n_accounts} accounts."
|
||||
)),
|
||||
});
|
||||
::tracing::event!(
|
||||
::tracing::Level::INFO,
|
||||
account_id = 0,
|
||||
"Starting background fetch for {n_accounts} accounts."
|
||||
);
|
||||
let mut set = JoinSet::new();
|
||||
for account in accounts {
|
||||
set.spawn(async move {
|
||||
@@ -375,17 +412,41 @@ impl Accounts {
|
||||
"Finished background fetch for {n_accounts} accounts."
|
||||
)),
|
||||
});
|
||||
::tracing::event!(
|
||||
::tracing::Level::INFO,
|
||||
account_id = 0,
|
||||
"Finished background fetch for {n_accounts} 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
|
||||
{
|
||||
@@ -393,15 +454,26 @@ impl Accounts {
|
||||
id: 0,
|
||||
typ: EventType::Warning("Background fetch timed out.".to_string()),
|
||||
});
|
||||
::tracing::event!(
|
||||
::tracing::Level::WARN,
|
||||
account_id = 0,
|
||||
"Background fetch timed out."
|
||||
);
|
||||
}
|
||||
events.emit(Event {
|
||||
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 +486,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.
|
||||
@@ -487,6 +591,7 @@ impl Config {
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
async fn create_lock_task(dir: PathBuf) -> Result<Option<JoinHandle<anyhow::Result<()>>>> {
|
||||
let lockfile = dir.join(LOCKFILE_NAME);
|
||||
let mut lock = fd_lock::RwLock::new(fs::File::create(lockfile).await?);
|
||||
@@ -604,13 +709,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 {
|
||||
@@ -654,6 +758,7 @@ impl Config {
|
||||
}
|
||||
|
||||
/// Creates a new account in the account manager directory.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
async fn new_account(&mut self) -> Result<AccountConfig> {
|
||||
let id = {
|
||||
let id = self.inner.next_id;
|
||||
@@ -743,6 +848,7 @@ impl Config {
|
||||
///
|
||||
/// Without this workaround removing account may fail on Windows with an error
|
||||
/// "The process cannot access the file because it is being used by another process. (os error 32)".
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
async fn try_many_times<F, Fut, T>(f: F) -> std::result::Result<(), T>
|
||||
where
|
||||
F: Fn() -> Fut,
|
||||
@@ -814,6 +920,26 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_account_new_empty_existing_dir() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts");
|
||||
|
||||
// A non-empty directory without accounts.toml should fail.
|
||||
fs::create_dir_all(&p).await.unwrap();
|
||||
fs::write(p.join("stray_file.txt"), b"hello").await.unwrap();
|
||||
assert!(Accounts::new(p.clone(), true).await.is_err());
|
||||
|
||||
// Clean up to an empty directory.
|
||||
fs::remove_file(p.join("stray_file.txt")).await.unwrap();
|
||||
|
||||
// An empty directory without accounts.toml should succeed.
|
||||
let mut accounts = Accounts::new(p.clone(), true).await.unwrap();
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
let id = accounts.add_account().await.unwrap();
|
||||
assert_eq!(id, 1);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_account_new_open_conflict() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -47,11 +47,11 @@ pub struct Aheader {
|
||||
pub public_key: SignedPublicKey,
|
||||
pub prefer_encrypt: EncryptPreference,
|
||||
|
||||
// Whether `_verified` attribute is present.
|
||||
//
|
||||
// `_verified` attribute is an extension to `Autocrypt-Gossip`
|
||||
// header that is used to tell that the sender
|
||||
// marked this key as verified.
|
||||
/// Whether `_verified` attribute is present.
|
||||
///
|
||||
/// `_verified` attribute is an extension to `Autocrypt-Gossip`
|
||||
/// header that is used to tell that the sender
|
||||
/// marked this key as verified.
|
||||
pub verified: bool,
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ impl fmt::Display for Aheader {
|
||||
let keydata = self.public_key.to_base64().chars().enumerate().fold(
|
||||
String::new(),
|
||||
|mut res, (i, c)| {
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
if i % 78 == 78 - "keydata=".len() {
|
||||
res.push(' ')
|
||||
}
|
||||
@@ -107,13 +108,11 @@ impl FromStr for Aheader {
|
||||
.remove("keydata")
|
||||
.context("keydata attribute is not found")
|
||||
.and_then(|raw| {
|
||||
SignedPublicKey::from_base64(&raw).context("autocrypt key cannot be decoded")
|
||||
})
|
||||
.and_then(|key| {
|
||||
key.verify()
|
||||
.and(Ok(key))
|
||||
.context("autocrypt key cannot be verified")
|
||||
SignedPublicKey::from_base64(&raw).context("Autocrypt key cannot be decoded")
|
||||
})?;
|
||||
public_key
|
||||
.verify_bindings()
|
||||
.context("Autocrypt key cannot be verified")?;
|
||||
|
||||
let prefer_encrypt = attributes
|
||||
.remove("prefer-encrypt")
|
||||
|
||||
62
src/blob.rs
62
src/blob.rs
@@ -1,6 +1,6 @@
|
||||
//! # Blob directory management.
|
||||
|
||||
use core::cmp::max;
|
||||
use std::cmp::max;
|
||||
use std::io::{Cursor, Seek};
|
||||
use std::iter::FusedIterator;
|
||||
use std::mem;
|
||||
@@ -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,12 +251,12 @@ 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.
|
||||
pub async fn recode_to_avatar_size(&mut self, context: &Context) -> Result<()> {
|
||||
let (img_wh, max_bytes) =
|
||||
let (max_wh, max_bytes) =
|
||||
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
@@ -268,7 +273,7 @@ impl<'a> BlobObject<'a> {
|
||||
let is_avatar = true;
|
||||
self.check_or_recode_to_size(
|
||||
context, None, // The name of an avatar doesn't matter
|
||||
viewtype, img_wh, max_bytes, is_avatar,
|
||||
viewtype, max_wh, max_bytes, is_avatar,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
@@ -289,7 +294,7 @@ impl<'a> BlobObject<'a> {
|
||||
name: Option<String>,
|
||||
viewtype: &mut Viewtype,
|
||||
) -> Result<String> {
|
||||
let (img_wh, max_bytes) =
|
||||
let (max_wh, max_bytes) =
|
||||
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
@@ -300,13 +305,15 @@ impl<'a> BlobObject<'a> {
|
||||
MediaQuality::Worse => (constants::WORSE_IMAGE_SIZE, constants::WORSE_IMAGE_BYTES),
|
||||
};
|
||||
let is_avatar = false;
|
||||
self.check_or_recode_to_size(context, name, viewtype, img_wh, max_bytes, is_avatar)
|
||||
self.check_or_recode_to_size(context, name, viewtype, max_wh, max_bytes, is_avatar)
|
||||
}
|
||||
|
||||
/// Checks or recodes the image so that it fits into limits on width/height and byte size.
|
||||
/// Checks or recodes the image so that it fits into limits on width/height and/or byte size.
|
||||
///
|
||||
/// If `!is_avatar`, then if `max_bytes` is exceeded, reduces the image to `img_wh` and proceeds
|
||||
/// with the result without rechecking.
|
||||
/// If `!is_avatar`, then if `max_bytes` is exceeded, reduces the image to `max_wh` and proceeds
|
||||
/// with the result (even if `max_bytes` is still exceeded).
|
||||
///
|
||||
/// If `is_avatar`, the resolution will be reduced in a loop until the image fits `max_bytes`.
|
||||
///
|
||||
/// This modifies the blob object in-place.
|
||||
///
|
||||
@@ -314,12 +321,13 @@ impl<'a> BlobObject<'a> {
|
||||
/// then the updated user-visible filename will be returned;
|
||||
/// this may be necessary because the format may be changed to JPG,
|
||||
/// i.e. "image.png" -> "image.jpg".
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
fn check_or_recode_to_size(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
name: Option<String>,
|
||||
viewtype: &mut Viewtype,
|
||||
mut img_wh: u32,
|
||||
max_wh: u32,
|
||||
max_bytes: usize,
|
||||
is_avatar: bool,
|
||||
) -> Result<String> {
|
||||
@@ -381,7 +389,14 @@ impl<'a> BlobObject<'a> {
|
||||
_ => img,
|
||||
};
|
||||
|
||||
let exceeds_wh = img.width() > img_wh || img.height() > img_wh;
|
||||
// max_wh is the maximum image width and height, i.e. the resolution-limit.
|
||||
// target_wh target-resolution for resizing the image.
|
||||
let exceeds_wh = img.width() > max_wh || img.height() > max_wh;
|
||||
let mut target_wh = if exceeds_wh {
|
||||
max_wh
|
||||
} else {
|
||||
max(img.width(), img.height())
|
||||
};
|
||||
let exceeds_max_bytes = nr_bytes > max_bytes as u64;
|
||||
|
||||
let jpeg_quality = 75;
|
||||
@@ -420,15 +435,6 @@ impl<'a> BlobObject<'a> {
|
||||
});
|
||||
|
||||
if do_scale {
|
||||
if !exceeds_wh {
|
||||
img_wh = max(img.width(), img.height());
|
||||
// PNGs and WebPs may be huge because of animation, which is lost by the `image`
|
||||
// crate when recoding, so don't scale them down.
|
||||
if matches!(fmt, ImageFormat::Jpeg) || !encoded.is_empty() {
|
||||
img_wh = img_wh * 2 / 3;
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
if mem::take(&mut add_white_bg) {
|
||||
self::add_white_bg(&mut img);
|
||||
@@ -443,9 +449,9 @@ impl<'a> BlobObject<'a> {
|
||||
// usually has less pixels by cropping, UI that needs to wait anyways,
|
||||
// and also benefits from slightly better (5%) encoding of Triangle-filtered images.
|
||||
let new_img = if is_avatar {
|
||||
img.resize(img_wh, img_wh, image::imageops::FilterType::Triangle)
|
||||
img.resize(target_wh, target_wh, image::imageops::FilterType::Triangle)
|
||||
} else {
|
||||
img.thumbnail(img_wh, img_wh)
|
||||
img.thumbnail(target_wh, target_wh)
|
||||
};
|
||||
|
||||
if encoded_img_exceeds_bytes(
|
||||
@@ -456,19 +462,19 @@ impl<'a> BlobObject<'a> {
|
||||
&mut encoded,
|
||||
)? && is_avatar
|
||||
{
|
||||
if img_wh < 20 {
|
||||
if target_wh < 20 {
|
||||
return Err(format_err!(
|
||||
"Failed to scale image to below {max_bytes}B.",
|
||||
));
|
||||
}
|
||||
|
||||
img_wh = img_wh * 2 / 3;
|
||||
target_wh = target_wh * 2 / 3;
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"Final scaled-down image size: {}B ({}px).",
|
||||
encoded.len(),
|
||||
img_wh
|
||||
target_wh
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -173,11 +173,8 @@ async fn test_selfavatar_outside_blobdir() {
|
||||
.unwrap();
|
||||
let avatar_blob = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||
let avatar_path = Path::new(&avatar_blob);
|
||||
assert!(
|
||||
avatar_blob.ends_with("7dde69e06b5ae6c27520a436bbfd65b.jpg"),
|
||||
"The avatar filename should be its hash, put instead it's {avatar_blob}"
|
||||
);
|
||||
let scaled_avatar_size = file_size(avatar_path).await;
|
||||
info!(&t, "Scaled avatar size: {scaled_avatar_size}.");
|
||||
assert!(scaled_avatar_size < avatar_bytes.len() as u64);
|
||||
|
||||
check_image_size(avatar_src, 1000, 1000);
|
||||
@@ -187,6 +184,11 @@ async fn test_selfavatar_outside_blobdir() {
|
||||
constants::BALANCED_AVATAR_SIZE,
|
||||
);
|
||||
|
||||
assert!(
|
||||
avatar_blob.ends_with("2a048b6fcd86448032b854ea1ad7608.jpg"),
|
||||
"The avatar filename should be its hash, but instead it's {avatar_blob}"
|
||||
);
|
||||
|
||||
let mut blob = BlobObject::create_and_deduplicate(&t, avatar_path, avatar_path).unwrap();
|
||||
let viewtype = &mut Viewtype::Image;
|
||||
let strict_limits = true;
|
||||
@@ -796,3 +798,56 @@ async fn test_create_and_deduplicate_from_bytes() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that an image that already fits into the width limit,
|
||||
/// but not the bytes limit,
|
||||
/// is compressed without changing the resolution.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_without_downscaling() -> Result<()> {
|
||||
let t = &TestContext::new().await;
|
||||
|
||||
let image = include_bytes!("../../test-data/image/screenshot120x120.jpg");
|
||||
const { assert!(120 < constants::WORSE_AVATAR_SIZE) };
|
||||
|
||||
for is_avatar in [true, false] {
|
||||
let mut blob =
|
||||
BlobObject::create_and_deduplicate_from_bytes(t, image, "image.jpg").unwrap();
|
||||
let image_path = blob.to_abs_path();
|
||||
check_image_size(&image_path, 120, 120);
|
||||
|
||||
assert!(
|
||||
fs::metadata(&image_path).await.unwrap().len() > constants::WORSE_AVATAR_BYTES as u64
|
||||
);
|
||||
|
||||
// Repeat the check, because a second call to `check_or_recode_to_size()`
|
||||
// is not supposed to change anything:
|
||||
let mut imgs = vec![];
|
||||
for _ in 0..2 {
|
||||
let mut viewtype = Viewtype::Image;
|
||||
let new_name = blob.check_or_recode_to_size(
|
||||
t,
|
||||
Some("image.jpg".to_string()),
|
||||
&mut viewtype,
|
||||
constants::WORSE_AVATAR_SIZE,
|
||||
constants::WORSE_AVATAR_BYTES,
|
||||
is_avatar,
|
||||
)?;
|
||||
let image_path = blob.to_abs_path();
|
||||
assert_eq!(new_name, "image.jpg"); // The name shall not have changed
|
||||
assert_eq!(viewtype, Viewtype::Image); // The viewtype shall not have changed
|
||||
let img = check_image_size(&image_path, 120, 120); // The resolution shall not have changed
|
||||
imgs.push(img);
|
||||
|
||||
let new_image_bytes = fs::metadata(&image_path).await.unwrap().len();
|
||||
assert!(
|
||||
new_image_bytes < constants::WORSE_AVATAR_BYTES as u64,
|
||||
"The new image size, {new_image_bytes}, should be lower than {}, is_avatar={is_avatar}",
|
||||
constants::WORSE_AVATAR_BYTES
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(imgs[0], imgs[1]);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
340
src/calls.rs
340
src/calls.rs
@@ -4,21 +4,23 @@
|
||||
//! This means, the "Call ID" is a "Message ID" - similar to Webxdc IDs.
|
||||
use crate::chat::ChatIdBlocked;
|
||||
use crate::chat::{Chat, ChatId, send_msg};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Blocked, Chattype};
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::context::{Context, WeakContext};
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::log::{info, warn};
|
||||
use crate::message::{self, Message, MsgId, Viewtype};
|
||||
use crate::log::warn;
|
||||
use crate::message::{Message, MsgId, Viewtype, markseen_msgs};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::net::dns::lookup_host_with_cache;
|
||||
use crate::param::Param;
|
||||
use crate::tools::time;
|
||||
use crate::stock_str;
|
||||
use crate::tools::{normalize_text, time};
|
||||
use anyhow::{Context as _, Result, ensure};
|
||||
use sdp::SessionDescription;
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use num_traits::FromPrimitive;
|
||||
use serde::Serialize;
|
||||
use std::io::Cursor;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use tokio::task;
|
||||
@@ -33,7 +35,7 @@ use tokio::time::sleep;
|
||||
///
|
||||
/// For the caller, this means they should also not wait longer,
|
||||
/// as the callee won't start the call afterwards.
|
||||
const RINGING_SECONDS: i64 = 60;
|
||||
const RINGING_SECONDS: i64 = 120;
|
||||
|
||||
// For persisting parameters in the call, we use Param::Arg*
|
||||
|
||||
@@ -77,6 +79,7 @@ impl CallInfo {
|
||||
}
|
||||
|
||||
fn remaining_ring_seconds(&self) -> i64 {
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
let remaining_seconds = self.msg.timestamp_sent + RINGING_SECONDS - time();
|
||||
remaining_seconds.clamp(0, RINGING_SECONDS)
|
||||
}
|
||||
@@ -86,7 +89,7 @@ impl CallInfo {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET txt=?, txt_normalized=? WHERE id=?",
|
||||
(text, message::normalize_text(text), self.msg.id),
|
||||
(text, normalize_text(text), self.msg.id),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -101,10 +104,14 @@ impl CallInfo {
|
||||
};
|
||||
|
||||
if self.is_incoming() {
|
||||
self.update_text(context, &format!("Incoming call\n{duration}"))
|
||||
let incoming_call_str =
|
||||
stock_str::incoming_call(context, self.has_video_initially()).await;
|
||||
self.update_text(context, &format!("{incoming_call_str}\n{duration}"))
|
||||
.await?;
|
||||
} else {
|
||||
self.update_text(context, &format!("Outgoing call\n{duration}"))
|
||||
let outgoing_call_str =
|
||||
stock_str::outgoing_call(context, self.has_video_initially()).await;
|
||||
self.update_text(context, &format!("{outgoing_call_str}\n{duration}"))
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -123,6 +130,14 @@ impl CallInfo {
|
||||
self.msg.param.exists(CALL_ACCEPTED_TIMESTAMP)
|
||||
}
|
||||
|
||||
/// Returns true if the call is started as a video call.
|
||||
pub fn has_video_initially(&self) -> bool {
|
||||
self.msg
|
||||
.param
|
||||
.get_bool(Param::WebrtcHasVideoInitially)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Returns true if the call is missed
|
||||
/// because the caller canceled it
|
||||
/// explicitly before ringing stopped.
|
||||
@@ -161,6 +176,7 @@ impl CallInfo {
|
||||
}
|
||||
|
||||
/// Returns call duration in seconds.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub fn duration_seconds(&self) -> i64 {
|
||||
if let (Some(start), Some(end)) = (
|
||||
self.msg.param.get_i64(CALL_ACCEPTED_TIMESTAMP),
|
||||
@@ -182,6 +198,7 @@ impl Context {
|
||||
&self,
|
||||
chat_id: ChatId,
|
||||
place_call_info: String,
|
||||
has_video_initially: bool,
|
||||
) -> Result<MsgId> {
|
||||
let chat = Chat::load_from_db(self, chat_id).await?;
|
||||
ensure!(
|
||||
@@ -190,17 +207,21 @@ impl Context {
|
||||
);
|
||||
ensure!(!chat.is_self_talk(), "Cannot call self");
|
||||
|
||||
let outgoing_call_str = stock_str::outgoing_call(self, has_video_initially).await;
|
||||
let mut call = Message {
|
||||
viewtype: Viewtype::Call,
|
||||
text: "Outgoing call".into(),
|
||||
text: outgoing_call_str,
|
||||
..Default::default()
|
||||
};
|
||||
call.param.set(Param::WebrtcRoom, &place_call_info);
|
||||
call.param
|
||||
.set_int(Param::WebrtcHasVideoInitially, has_video_initially.into());
|
||||
call.id = send_msg(self, chat_id, &mut call).await?;
|
||||
|
||||
let wait = RINGING_SECONDS;
|
||||
let context = self.get_weak_context();
|
||||
task::spawn(Context::emit_end_call_if_unaccepted(
|
||||
self.clone(),
|
||||
context,
|
||||
wait.try_into()?,
|
||||
call.id,
|
||||
));
|
||||
@@ -228,6 +249,7 @@ impl Context {
|
||||
if chat.is_contact_request() {
|
||||
chat.id.accept(self).await?;
|
||||
}
|
||||
markseen_msgs(self, vec![call_id]).await?;
|
||||
|
||||
// send an acceptance message around: to the caller as well as to the other devices of the callee
|
||||
let mut msg = Message {
|
||||
@@ -244,6 +266,7 @@ impl Context {
|
||||
self.emit_event(EventType::IncomingCallAccepted {
|
||||
msg_id: call.msg.id,
|
||||
chat_id: call.msg.chat_id,
|
||||
from_this_device: true,
|
||||
});
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
Ok(())
|
||||
@@ -262,10 +285,13 @@ impl Context {
|
||||
if !call.is_accepted() {
|
||||
if call.is_incoming() {
|
||||
call.mark_as_ended(self).await?;
|
||||
call.update_text(self, "Declined call").await?;
|
||||
markseen_msgs(self, vec![call_id]).await?;
|
||||
let declined_call_str = stock_str::declined_call(self).await;
|
||||
call.update_text(self, &declined_call_str).await?;
|
||||
} else {
|
||||
call.mark_as_canceled(self).await?;
|
||||
call.update_text(self, "Canceled call").await?;
|
||||
let canceled_call_str = stock_str::canceled_call(self).await;
|
||||
call.update_text(self, &canceled_call_str).await?;
|
||||
}
|
||||
} else {
|
||||
call.mark_as_ended(self).await?;
|
||||
@@ -291,11 +317,12 @@ impl Context {
|
||||
}
|
||||
|
||||
async fn emit_end_call_if_unaccepted(
|
||||
context: Context,
|
||||
context: WeakContext,
|
||||
wait: u64,
|
||||
call_id: MsgId,
|
||||
) -> Result<()> {
|
||||
sleep(Duration::from_secs(wait)).await;
|
||||
let context = context.upgrade()?;
|
||||
let Some(mut call) = context.load_call_by_id(call_id).await? else {
|
||||
warn!(
|
||||
context,
|
||||
@@ -306,10 +333,12 @@ impl Context {
|
||||
if !call.is_accepted() && !call.is_ended() {
|
||||
if call.is_incoming() {
|
||||
call.mark_as_canceled(&context).await?;
|
||||
call.update_text(&context, "Missed call").await?;
|
||||
let missed_call_str = stock_str::missed_call(&context).await;
|
||||
call.update_text(&context, &missed_call_str).await?;
|
||||
} else {
|
||||
call.mark_as_ended(&context).await?;
|
||||
call.update_text(&context, "Canceled call").await?;
|
||||
let canceled_call_str = stock_str::canceled_call(&context).await;
|
||||
call.update_text(&context, &canceled_call_str).await?;
|
||||
}
|
||||
context.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
context.emit_event(EventType::CallEnded {
|
||||
@@ -334,48 +363,55 @@ impl Context {
|
||||
|
||||
if call.is_incoming() {
|
||||
if call.is_stale() {
|
||||
call.update_text(self, "Missed call").await?;
|
||||
let missed_call_str = stock_str::missed_call(self).await;
|
||||
call.update_text(self, &missed_call_str).await?;
|
||||
self.emit_incoming_msg(call.msg.chat_id, call_id); // notify missed call
|
||||
} else {
|
||||
call.update_text(self, "Incoming call").await?;
|
||||
let incoming_call_str =
|
||||
stock_str::incoming_call(self, call.has_video_initially()).await;
|
||||
call.update_text(self, &incoming_call_str).await?;
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id); // ringing calls are not additionally notified
|
||||
let has_video = match sdp_has_video(&call.place_call_info) {
|
||||
Ok(has_video) => has_video,
|
||||
Err(err) => {
|
||||
warn!(self, "Failed to determine if SDP offer has video: {err:#}.");
|
||||
false
|
||||
}
|
||||
let can_call_me = match who_can_call_me(self).await? {
|
||||
WhoCanCallMe::Contacts => ChatIdBlocked::lookup_by_contact(self, from_id)
|
||||
.await?
|
||||
.is_some_and(|chat_id_blocked| {
|
||||
match chat_id_blocked.blocked {
|
||||
Blocked::Not => true,
|
||||
Blocked::Yes | Blocked::Request => {
|
||||
// Do not notify about incoming calls
|
||||
// from contact requests and blocked contacts.
|
||||
//
|
||||
// User can still access the call and accept it
|
||||
// via the chat in case of contact requests.
|
||||
false
|
||||
}
|
||||
}
|
||||
}),
|
||||
WhoCanCallMe::Everybody => ChatIdBlocked::lookup_by_contact(self, from_id)
|
||||
.await?
|
||||
.is_none_or(|chat_id_blocked| chat_id_blocked.blocked != Blocked::Yes),
|
||||
WhoCanCallMe::Nobody => false,
|
||||
};
|
||||
if let Some(chat_id_blocked) =
|
||||
ChatIdBlocked::lookup_by_contact(self, from_id).await?
|
||||
{
|
||||
match chat_id_blocked.blocked {
|
||||
Blocked::Not => {
|
||||
self.emit_event(EventType::IncomingCall {
|
||||
msg_id: call.msg.id,
|
||||
chat_id: call.msg.chat_id,
|
||||
place_call_info: call.place_call_info.to_string(),
|
||||
has_video,
|
||||
});
|
||||
}
|
||||
Blocked::Yes | Blocked::Request => {
|
||||
// Do not notify about incoming calls
|
||||
// from contact requests and blocked contacts.
|
||||
//
|
||||
// User can still access the call and accept it
|
||||
// via the chat in case of contact requests.
|
||||
}
|
||||
}
|
||||
if can_call_me {
|
||||
self.emit_event(EventType::IncomingCall {
|
||||
msg_id: call.msg.id,
|
||||
chat_id: call.msg.chat_id,
|
||||
place_call_info: call.place_call_info.to_string(),
|
||||
has_video: call.has_video_initially(),
|
||||
});
|
||||
}
|
||||
let wait = call.remaining_ring_seconds();
|
||||
let context = self.get_weak_context();
|
||||
task::spawn(Context::emit_end_call_if_unaccepted(
|
||||
self.clone(),
|
||||
context,
|
||||
wait.try_into()?,
|
||||
call.msg.id,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
call.update_text(self, "Outgoing call").await?;
|
||||
let outgoing_call_str =
|
||||
stock_str::outgoing_call(self, call.has_video_initially()).await;
|
||||
call.update_text(self, &outgoing_call_str).await?;
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
}
|
||||
} else {
|
||||
@@ -397,6 +433,7 @@ impl Context {
|
||||
self.emit_event(EventType::IncomingCallAccepted {
|
||||
msg_id: call.msg.id,
|
||||
chat_id: call.msg.chat_id,
|
||||
from_this_device: false,
|
||||
});
|
||||
} else {
|
||||
let accept_call_info = mime_message
|
||||
@@ -425,19 +462,23 @@ impl Context {
|
||||
if call.is_incoming() {
|
||||
if from_id == ContactId::SELF {
|
||||
call.mark_as_ended(self).await?;
|
||||
call.update_text(self, "Declined call").await?;
|
||||
let declined_call_str = stock_str::declined_call(self).await;
|
||||
call.update_text(self, &declined_call_str).await?;
|
||||
} else {
|
||||
call.mark_as_canceled(self).await?;
|
||||
call.update_text(self, "Missed call").await?;
|
||||
let missed_call_str = stock_str::missed_call(self).await;
|
||||
call.update_text(self, &missed_call_str).await?;
|
||||
}
|
||||
} else {
|
||||
// outgoing
|
||||
if from_id == ContactId::SELF {
|
||||
call.mark_as_canceled(self).await?;
|
||||
call.update_text(self, "Canceled call").await?;
|
||||
let canceled_call_str = stock_str::canceled_call(self).await;
|
||||
call.update_text(self, &canceled_call_str).await?;
|
||||
} else {
|
||||
call.mark_as_ended(self).await?;
|
||||
call.update_text(self, "Declined call").await?;
|
||||
let declined_call_str = stock_str::declined_call(self).await;
|
||||
call.update_text(self, &declined_call_str).await?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -493,19 +534,6 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if SDP offer has a video.
|
||||
pub fn sdp_has_video(sdp: &str) -> Result<bool> {
|
||||
let mut cursor = Cursor::new(sdp);
|
||||
let session_description =
|
||||
SessionDescription::unmarshal(&mut cursor).context("Failed to parse SDP")?;
|
||||
for media_description in &session_description.media_descriptions {
|
||||
if media_description.media_name.media == "video" {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// State of the call for display in the message bubble.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum CallState {
|
||||
@@ -603,33 +631,7 @@ struct IceServer {
|
||||
pub credential: Option<String>,
|
||||
}
|
||||
|
||||
/// Creates JSON with ICE servers.
|
||||
async fn create_ice_servers(
|
||||
context: &Context,
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<String> {
|
||||
// Do not use cache because there is no TLS.
|
||||
let load_cache = false;
|
||||
let urls: Vec<String> = lookup_host_with_cache(context, hostname, port, "", load_cache)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|addr| format!("turn:{addr}"))
|
||||
.collect();
|
||||
|
||||
let ice_server = IceServer {
|
||||
urls,
|
||||
username: Some(username.to_string()),
|
||||
credential: Some(password.to_string()),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&[ice_server])?;
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
/// Creates JSON with ICE servers from a line received over IMAP METADATA.
|
||||
/// Creates ICE servers from a line received over IMAP METADATA.
|
||||
///
|
||||
/// IMAP METADATA returns a line such as
|
||||
/// `example.com:3478:1758650868:8Dqkyyu11MVESBqjbIylmB06rv8=`
|
||||
@@ -639,20 +641,107 @@ async fn create_ice_servers(
|
||||
/// while `8Dqkyyu11MVESBqjbIylmB06rv8=`
|
||||
/// is the password.
|
||||
pub(crate) async fn create_ice_servers_from_metadata(
|
||||
context: &Context,
|
||||
metadata: &str,
|
||||
) -> Result<(i64, String)> {
|
||||
) -> Result<(i64, Vec<UnresolvedIceServer>)> {
|
||||
let (hostname, rest) = metadata.split_once(':').context("Missing hostname")?;
|
||||
let (port, rest) = rest.split_once(':').context("Missing port")?;
|
||||
let port = u16::from_str(port).context("Failed to parse the port")?;
|
||||
let (ts, password) = rest.split_once(':').context("Missing timestamp")?;
|
||||
let expiration_timestamp = i64::from_str(ts).context("Failed to parse the timestamp")?;
|
||||
let ice_servers = create_ice_servers(context, hostname, port, ts, password).await?;
|
||||
let ice_servers = vec![UnresolvedIceServer::Turn {
|
||||
hostname: hostname.to_string(),
|
||||
port,
|
||||
username: ts.to_string(),
|
||||
credential: password.to_string(),
|
||||
}];
|
||||
Ok((expiration_timestamp, ice_servers))
|
||||
}
|
||||
|
||||
/// STUN or TURN server with unresolved DNS name.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum UnresolvedIceServer {
|
||||
/// STUN server.
|
||||
Stun { hostname: String, port: u16 },
|
||||
|
||||
/// TURN server with the username and password.
|
||||
Turn {
|
||||
hostname: String,
|
||||
port: u16,
|
||||
username: String,
|
||||
credential: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Resolves domain names of ICE servers.
|
||||
///
|
||||
/// On failure to resolve, logs the error
|
||||
/// and skips the server, but does not fail.
|
||||
pub(crate) async fn resolve_ice_servers(
|
||||
context: &Context,
|
||||
unresolved_ice_servers: Vec<UnresolvedIceServer>,
|
||||
) -> Result<String> {
|
||||
let mut result: Vec<IceServer> = Vec::new();
|
||||
|
||||
// Do not use cache because there is no TLS.
|
||||
let load_cache = false;
|
||||
|
||||
for unresolved_ice_server in unresolved_ice_servers {
|
||||
match unresolved_ice_server {
|
||||
UnresolvedIceServer::Stun { hostname, port } => {
|
||||
match lookup_host_with_cache(context, &hostname, port, "", load_cache).await {
|
||||
Ok(addrs) => {
|
||||
let urls: Vec<String> = addrs
|
||||
.into_iter()
|
||||
.map(|addr| format!("stun:{addr}"))
|
||||
.collect();
|
||||
let stun_server = IceServer {
|
||||
urls,
|
||||
username: None,
|
||||
credential: None,
|
||||
};
|
||||
result.push(stun_server);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to resolve STUN {hostname}:{port}: {err:#}."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
UnresolvedIceServer::Turn {
|
||||
hostname,
|
||||
port,
|
||||
username,
|
||||
credential,
|
||||
} => match lookup_host_with_cache(context, &hostname, port, "", load_cache).await {
|
||||
Ok(addrs) => {
|
||||
let urls: Vec<String> = addrs
|
||||
.into_iter()
|
||||
.map(|addr| format!("turn:{addr}"))
|
||||
.collect();
|
||||
let turn_server = IceServer {
|
||||
urls,
|
||||
username: Some(username),
|
||||
credential: Some(credential),
|
||||
};
|
||||
result.push(turn_server);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to resolve TURN {hostname}:{port}: {err:#}."
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
let json = serde_json::to_string(&result)?;
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
/// Creates JSON with ICE servers when no TURN servers are known.
|
||||
pub(crate) async fn create_fallback_ice_servers(context: &Context) -> Result<String> {
|
||||
pub(crate) fn create_fallback_ice_servers() -> Vec<UnresolvedIceServer> {
|
||||
// Do not use public STUN server from https://stunprotocol.org/.
|
||||
// It changes the hostname every year
|
||||
// (e.g. stunserver2025.stunprotocol.org
|
||||
@@ -660,25 +749,18 @@ pub(crate) async fn create_fallback_ice_servers(context: &Context) -> Result<Str
|
||||
// because of bandwidth costs:
|
||||
// <https://github.com/jselbie/stunserver/issues/50>
|
||||
|
||||
// We use nine.testrun.org for a default STUN server.
|
||||
let hostname = "nine.testrun.org";
|
||||
|
||||
// Do not use cache because there is no TLS.
|
||||
let load_cache = false;
|
||||
let urls: Vec<String> = lookup_host_with_cache(context, hostname, STUN_PORT, "", load_cache)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|addr| format!("stun:{addr}"))
|
||||
.collect();
|
||||
|
||||
let ice_server = IceServer {
|
||||
urls,
|
||||
username: None,
|
||||
credential: None,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&[ice_server])?;
|
||||
Ok(json)
|
||||
vec![
|
||||
UnresolvedIceServer::Stun {
|
||||
hostname: "nine.testrun.org".to_string(),
|
||||
port: STUN_PORT,
|
||||
},
|
||||
UnresolvedIceServer::Turn {
|
||||
hostname: "turn.delta.chat".to_string(),
|
||||
port: STUN_PORT,
|
||||
username: "public".to_string(),
|
||||
credential: "o4tR7yG4rG2slhXqRUf9zgmHz".to_string(),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/// Returns JSON with ICE servers.
|
||||
@@ -692,11 +774,39 @@ pub(crate) async fn create_fallback_ice_servers(context: &Context) -> Result<Str
|
||||
/// <https://github.com/deltachat/deltachat-desktop/issues/5447>.
|
||||
pub async fn ice_servers(context: &Context) -> Result<String> {
|
||||
if let Some(ref metadata) = *context.metadata.read().await {
|
||||
Ok(metadata.ice_servers.clone())
|
||||
let ice_servers = resolve_ice_servers(context, metadata.ice_servers.clone()).await?;
|
||||
Ok(ice_servers)
|
||||
} else {
|
||||
Ok("[]".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// "Who can call me" config options.
|
||||
#[derive(
|
||||
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||
)]
|
||||
#[repr(u8)]
|
||||
pub enum WhoCanCallMe {
|
||||
/// Everybody can call me if they are not blocked.
|
||||
///
|
||||
/// This includes contact requests.
|
||||
Everybody = 0,
|
||||
|
||||
/// Every contact who is not blocked and not a contact request, can call.
|
||||
#[default]
|
||||
Contacts = 1,
|
||||
|
||||
/// Nobody can call me.
|
||||
Nobody = 2,
|
||||
}
|
||||
|
||||
/// Returns currently configuration of the "who can call me" option.
|
||||
async fn who_can_call_me(context: &Context) -> Result<WhoCanCallMe> {
|
||||
let who_can_call_me =
|
||||
WhoCanCallMe::from_i32(context.get_config_int(Config::WhoCanCallMe).await?)
|
||||
.unwrap_or_default();
|
||||
Ok(who_can_call_me)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod calls_tests;
|
||||
|
||||
@@ -2,7 +2,8 @@ use super::*;
|
||||
use crate::chat::forward_msgs;
|
||||
use crate::config::Config;
|
||||
use crate::constants::DC_CHAT_ID_TRASH;
|
||||
use crate::receive_imf::{receive_imf, receive_imf_from_inbox};
|
||||
use crate::message::MessageState;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
|
||||
struct CallSetup {
|
||||
@@ -25,13 +26,6 @@ async fn assert_text(t: &TestContext, call_id: MsgId, text: &str) -> Result<()>
|
||||
const PLACE_INFO: &str = "v=0\r\no=alice 2890844526 2890844526 IN IP4 host.anywhere.com\r\ns=-\r\nc=IN IP4 host.anywhere.com\r\nt=0 0\r\nm=audio 62986 RTP/AVP 0 4 18\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:4 G723/8000\r\na=rtpmap:18 G729/8000\r\na=inactive\r\n";
|
||||
const ACCEPT_INFO: &str = "v=0\r\no=bob 2890844730 2890844731 IN IP4 host.example.com\r\ns=\r\nc=IN IP4 host.example.com\r\nt=0 0\r\nm=audio 54344 RTP/AVP 0 4\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:4 G723/8000\r\na=inactive\r\n";
|
||||
|
||||
/// Example from <https://datatracker.ietf.org/doc/rfc9143/>
|
||||
/// with `s= ` replaced with `s=-`.
|
||||
///
|
||||
/// `s=` cannot be empty according to RFC 3264,
|
||||
/// so it is more clear as `s=-`.
|
||||
const PLACE_INFO_VIDEO: &str = "v=0\r\no=alice 2890844526 2890844526 IN IP6 2001:db8::3\r\ns=-\r\nc=IN IP6 2001:db8::3\r\nt=0 0\r\na=group:BUNDLE foo bar\r\n\r\nm=audio 10000 RTP/AVP 0 8 97\r\nb=AS:200\r\na=mid:foo\r\na=rtcp-mux\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:97 iLBC/8000\r\na=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r\n\r\nm=video 10002 RTP/AVP 31 32\r\nb=AS:1000\r\na=mid:bar\r\na=rtcp-mux\r\na=rtpmap:31 H261/90000\r\na=rtpmap:32 MPV/90000\r\na=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r\n";
|
||||
|
||||
async fn setup_call() -> Result<CallSetup> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
@@ -52,7 +46,7 @@ async fn setup_call() -> Result<CallSetup> {
|
||||
bob2.create_chat(&alice).await;
|
||||
|
||||
let test_msg_id = alice
|
||||
.place_outgoing_call(alice_chat.id, PLACE_INFO.to_string())
|
||||
.place_outgoing_call(alice_chat.id, PLACE_INFO.to_string(), true)
|
||||
.await?;
|
||||
let sent1 = alice.pop_sent_msg().await;
|
||||
assert_eq!(sent1.sender_msg_id, test_msg_id);
|
||||
@@ -68,7 +62,8 @@ async fn setup_call() -> Result<CallSetup> {
|
||||
assert!(!info.is_incoming());
|
||||
assert!(!info.is_accepted());
|
||||
assert_eq!(info.place_call_info, PLACE_INFO);
|
||||
assert_text(t, m.id, "Outgoing call").await?;
|
||||
assert_eq!(info.has_video_initially(), true);
|
||||
assert_text(t, m.id, "Outgoing video call").await?;
|
||||
assert_eq!(call_state(t, m.id).await?, CallState::Alerting);
|
||||
}
|
||||
|
||||
@@ -89,7 +84,8 @@ async fn setup_call() -> Result<CallSetup> {
|
||||
assert!(info.is_incoming());
|
||||
assert!(!info.is_accepted());
|
||||
assert_eq!(info.place_call_info, PLACE_INFO);
|
||||
assert_text(t, m.id, "Incoming call").await?;
|
||||
assert_eq!(info.has_video_initially(), true);
|
||||
assert_text(t, m.id, "Incoming video call").await?;
|
||||
assert_eq!(call_state(t, m.id).await?, CallState::Alerting);
|
||||
}
|
||||
|
||||
@@ -120,9 +116,28 @@ async fn accept_call() -> Result<CallSetup> {
|
||||
// Bob accepts the incoming call
|
||||
bob.accept_incoming_call(bob_call.id, ACCEPT_INFO.to_string())
|
||||
.await?;
|
||||
assert_text(&bob, bob_call.id, "Incoming call").await?;
|
||||
assert_eq!(bob_call.id.get_state(&bob).await?, MessageState::InSeen);
|
||||
// Bob sends an MDN to Alice.
|
||||
assert_eq!(
|
||||
bob.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM smtp_mdns WHERE msg_id=? AND from_id=?",
|
||||
(bob_call.id, bob_call.from_id)
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
assert_text(&bob, bob_call.id, "Incoming video call").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
|
||||
.get_matching(|evt| {
|
||||
matches!(
|
||||
evt,
|
||||
EventType::IncomingCallAccepted {
|
||||
from_this_device: true,
|
||||
..
|
||||
}
|
||||
)
|
||||
})
|
||||
.await;
|
||||
let sent2 = bob.pop_sent_msg().await;
|
||||
let info = bob
|
||||
@@ -134,9 +149,17 @@ async fn accept_call() -> Result<CallSetup> {
|
||||
assert_eq!(call_state(&bob, bob_call.id).await?, CallState::Active);
|
||||
|
||||
bob2.recv_msg_trash(&sent2).await;
|
||||
assert_text(&bob, bob_call.id, "Incoming call").await?;
|
||||
assert_text(&bob, bob_call.id, "Incoming video call").await?;
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
|
||||
.get_matching(|evt| {
|
||||
matches!(
|
||||
evt,
|
||||
EventType::IncomingCallAccepted {
|
||||
from_this_device: false,
|
||||
..
|
||||
}
|
||||
)
|
||||
})
|
||||
.await;
|
||||
let info = bob2
|
||||
.load_call_by_id(bob2_call.id)
|
||||
@@ -147,7 +170,7 @@ async fn accept_call() -> Result<CallSetup> {
|
||||
|
||||
// Alice receives the acceptance message
|
||||
alice.recv_msg_trash(&sent2).await;
|
||||
assert_text(&alice, alice_call.id, "Outgoing call").await?;
|
||||
assert_text(&alice, alice_call.id, "Outgoing video call").await?;
|
||||
let ev = alice
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::OutgoingCallAccepted { .. }))
|
||||
@@ -169,7 +192,7 @@ async fn accept_call() -> Result<CallSetup> {
|
||||
assert_eq!(call_state(&alice, alice_call.id).await?, CallState::Active);
|
||||
|
||||
alice2.recv_msg_trash(&sent2).await;
|
||||
assert_text(&alice2, alice2_call.id, "Outgoing call").await?;
|
||||
assert_text(&alice2, alice2_call.id, "Outgoing video call").await?;
|
||||
alice2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::OutgoingCallAccepted { .. }))
|
||||
@@ -205,10 +228,21 @@ async fn test_accept_call_callee_ends() -> Result<()> {
|
||||
bob2_call,
|
||||
..
|
||||
} = accept_call().await?;
|
||||
assert_eq!(bob_call.id.get_state(&bob).await?, MessageState::InSeen);
|
||||
|
||||
// Bob has accepted the call and also ends it
|
||||
bob.end_call(bob_call.id).await?;
|
||||
assert_text(&bob, bob_call.id, "Incoming call\n<1 minute").await?;
|
||||
// Bob sends an MDN to Alice.
|
||||
assert_eq!(
|
||||
bob.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM smtp_mdns WHERE msg_id=? AND from_id=?",
|
||||
(bob_call.id, bob_call.from_id)
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
assert_text(&bob, bob_call.id, "Incoming video call\n<1 minute").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
@@ -219,7 +253,7 @@ async fn test_accept_call_callee_ends() -> Result<()> {
|
||||
));
|
||||
|
||||
bob2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob2, bob2_call.id, "Incoming call\n<1 minute").await?;
|
||||
assert_text(&bob2, bob2_call.id, "Incoming video call\n<1 minute").await?;
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
@@ -230,7 +264,7 @@ async fn test_accept_call_callee_ends() -> Result<()> {
|
||||
|
||||
// Alice receives the ending message
|
||||
alice.recv_msg_trash(&sent3).await;
|
||||
assert_text(&alice, alice_call.id, "Outgoing call\n<1 minute").await?;
|
||||
assert_text(&alice, alice_call.id, "Outgoing video call\n<1 minute").await?;
|
||||
alice
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
@@ -241,7 +275,7 @@ async fn test_accept_call_callee_ends() -> Result<()> {
|
||||
));
|
||||
|
||||
alice2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&alice2, alice2_call.id, "Outgoing call\n<1 minute").await?;
|
||||
assert_text(&alice2, alice2_call.id, "Outgoing video call\n<1 minute").await?;
|
||||
alice2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
@@ -271,7 +305,7 @@ async fn test_accept_call_caller_ends() -> Result<()> {
|
||||
|
||||
// Bob has accepted the call but Alice ends it
|
||||
alice.end_call(alice_call.id).await?;
|
||||
assert_text(&alice, alice_call.id, "Outgoing call\n<1 minute").await?;
|
||||
assert_text(&alice, alice_call.id, "Outgoing video call\n<1 minute").await?;
|
||||
alice
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
@@ -283,7 +317,7 @@ async fn test_accept_call_caller_ends() -> Result<()> {
|
||||
));
|
||||
|
||||
alice2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&alice2, alice2_call.id, "Outgoing call\n<1 minute").await?;
|
||||
assert_text(&alice2, alice2_call.id, "Outgoing video call\n<1 minute").await?;
|
||||
alice2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
@@ -295,7 +329,7 @@ async fn test_accept_call_caller_ends() -> Result<()> {
|
||||
|
||||
// Bob receives the ending message
|
||||
bob.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob, bob_call.id, "Incoming call\n<1 minute").await?;
|
||||
assert_text(&bob, bob_call.id, "Incoming video call\n<1 minute").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
@@ -305,7 +339,7 @@ async fn test_accept_call_caller_ends() -> Result<()> {
|
||||
));
|
||||
|
||||
bob2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob2, bob2_call.id, "Incoming call\n<1 minute").await?;
|
||||
assert_text(&bob2, bob2_call.id, "Incoming video call\n<1 minute").await?;
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
@@ -333,8 +367,18 @@ async fn test_callee_rejects_call() -> Result<()> {
|
||||
} = setup_call().await?;
|
||||
|
||||
// Bob has accepted Alice before, but does not want to talk with Alice
|
||||
bob_call.chat_id.accept(&bob).await?;
|
||||
bob.end_call(bob_call.id).await?;
|
||||
assert_eq!(bob_call.id.get_state(&bob).await?, MessageState::InSeen);
|
||||
// Bob sends an MDN to Alice.
|
||||
assert_eq!(
|
||||
bob.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM smtp_mdns WHERE msg_id=? AND from_id=?",
|
||||
(bob_call.id, bob_call.from_id)
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
assert_text(&bob, bob_call.id, "Declined call").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
@@ -375,6 +419,35 @@ async fn test_callee_rejects_call() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_callee_sees_contact_request_call() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_chat = alice.create_chat(bob).await;
|
||||
alice
|
||||
.place_outgoing_call(alice_chat.id, PLACE_INFO.to_string(), true)
|
||||
.await?;
|
||||
let sent1 = alice.pop_sent_msg().await;
|
||||
let bob_call = bob.recv_msg(&sent1).await;
|
||||
// Bob can't end_call() because the contact request isn't accepted, but he can mark the call as
|
||||
// seen.
|
||||
markseen_msgs(bob, vec![bob_call.id]).await?;
|
||||
assert_eq!(bob_call.id.get_state(bob).await?, MessageState::InSeen);
|
||||
// Bob sends an MDN only to self so that an unaccepted contact can't know anything.
|
||||
assert_eq!(
|
||||
bob.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM smtp_mdns WHERE msg_id=? AND from_id=?",
|
||||
(bob_call.id, ContactId::SELF)
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_caller_cancels_call() -> Result<()> {
|
||||
// Alice calls Bob
|
||||
@@ -425,7 +498,7 @@ async fn test_caller_cancels_call() -> Result<()> {
|
||||
// Test that message summary says it is a missed call.
|
||||
let bob_call_msg = Message::load_from_db(&bob, bob_call.id).await?;
|
||||
let summary = bob_call_msg.get_summary(&bob, None).await?;
|
||||
assert_eq!(summary.text, "📞 Missed call");
|
||||
assert_eq!(summary.text, "🎥 Missed call");
|
||||
|
||||
bob2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob2, bob2_call.id, "Missed call").await?;
|
||||
@@ -525,13 +598,6 @@ async fn test_update_call_text() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sdp_has_video() {
|
||||
assert!(sdp_has_video("foobar").is_err());
|
||||
assert_eq!(sdp_has_video(PLACE_INFO).unwrap(), false);
|
||||
assert_eq!(sdp_has_video(PLACE_INFO_VIDEO).unwrap(), true);
|
||||
}
|
||||
|
||||
/// Tests that calls are forwarded as text messages.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_forward_call() -> Result<()> {
|
||||
@@ -542,7 +608,7 @@ async fn test_forward_call() -> Result<()> {
|
||||
|
||||
let alice_bob_chat = alice.create_chat(bob).await;
|
||||
let alice_msg_id = alice
|
||||
.place_outgoing_call(alice_bob_chat.id, PLACE_INFO.to_string())
|
||||
.place_outgoing_call(alice_bob_chat.id, PLACE_INFO.to_string(), true)
|
||||
.await
|
||||
.context("Failed to place a call")?;
|
||||
let alice_call = Message::load_from_db(alice, alice_msg_id).await?;
|
||||
@@ -610,65 +676,3 @@ async fn test_end_text_call() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that partially downloaded "call ended"
|
||||
/// messages are not processed.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_partial_calls() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
|
||||
let seen = false;
|
||||
|
||||
// The messages in the test
|
||||
// have no `Date` on purpose,
|
||||
// so they are treated as new.
|
||||
let received_call = receive_imf(
|
||||
alice,
|
||||
b"From: bob@example.net\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <first@example.net>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Content: call\n\
|
||||
Chat-Webrtc-Room: YWFhYWFhYWFhCg==\n\
|
||||
\n\
|
||||
Hello, this is a call\n",
|
||||
seen,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_eq!(received_call.msg_ids.len(), 1);
|
||||
let call_msg = Message::load_from_db(alice, received_call.msg_ids[0])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(call_msg.viewtype, Viewtype::Call);
|
||||
assert_eq!(call_state(alice, call_msg.id).await?, CallState::Alerting);
|
||||
|
||||
let imf_raw = b"From: bob@example.net\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <second@example.net>\n\
|
||||
In-Reply-To: <first@example.net>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Content: call-ended\n\
|
||||
\n\
|
||||
Call ended\n";
|
||||
receive_imf_from_inbox(
|
||||
alice,
|
||||
"second@example.net",
|
||||
imf_raw,
|
||||
seen,
|
||||
Some(imf_raw.len().try_into().unwrap()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// The call is still not ended.
|
||||
assert_eq!(call_state(alice, call_msg.id).await?, CallState::Alerting);
|
||||
|
||||
// Fully downloading the message ends the call.
|
||||
receive_imf_from_inbox(alice, "second@example.net", imf_raw, seen, None)
|
||||
.await
|
||||
.context("Failed to fully download end call message")?;
|
||||
assert_eq!(call_state(alice, call_msg.id).await?, CallState::Missed);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
1228
src/chat.rs
1228
src/chat.rs
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
110
src/chatlist.rs
110
src/chatlist.rs
@@ -76,7 +76,7 @@ impl Chatlist {
|
||||
/// the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are *any* archived
|
||||
/// chats
|
||||
/// - the flag DC_GCL_FOR_FORWARDING sorts "Saved messages" to the top of the chatlist
|
||||
/// and hides the device-chat and contact requests
|
||||
/// and hides the device-chat, contact requests and incoming broadcasts.
|
||||
/// typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
|
||||
/// - if the flag DC_GCL_NO_SPECIALS is set, archive link is not added
|
||||
/// to the list (may be used eg. for selecting chats on forwarding, the flag is
|
||||
@@ -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 {
|
||||
@@ -192,10 +185,10 @@ impl Chatlist {
|
||||
warn!(context, "Cannot update special chat names: {err:#}.")
|
||||
}
|
||||
|
||||
let str_like_cmd = format!("%{query}%");
|
||||
let str_like_cmd = format!("%{}%", query.to_lowercase());
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
.query_map_vec(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
@@ -208,13 +201,12 @@ impl Chatlist {
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1)
|
||||
WHERE c.id>9 AND c.id!=?2
|
||||
AND c.blocked!=1
|
||||
AND c.name LIKE ?3
|
||||
AND IFNULL(c.name_normalized,c.name) LIKE ?3
|
||||
AND (NOT ?4 OR EXISTS (SELECT 1 FROM msgs m WHERE m.chat_id = c.id AND m.state == ?5 AND hidden=0))
|
||||
GROUP BY c.id
|
||||
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,11 +221,12 @@ 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
|
||||
&& param.get(Param::ListPost).is_none_or_empty()
|
||||
if typ == Chattype::InBroadcast
|
||||
|| (typ == Chattype::Mailinglist
|
||||
&& param.get(Param::ListPost).is_none_or_empty())
|
||||
{
|
||||
None
|
||||
} else {
|
||||
@@ -243,7 +236,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 +264,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 +282,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 {
|
||||
@@ -406,8 +397,6 @@ impl Chatlist {
|
||||
if lastmsg.from_id == ContactId::SELF {
|
||||
None
|
||||
} else if chat.typ == Chattype::Group
|
||||
|| chat.typ == Chattype::OutBroadcast
|
||||
|| chat.typ == Chattype::InBroadcast
|
||||
|| chat.typ == Chattype::Mailinglist
|
||||
|| chat.is_self_talk()
|
||||
{
|
||||
@@ -481,10 +470,11 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::chat::save_msgs;
|
||||
use crate::chat::{
|
||||
add_contact_to_chat, create_group, get_chat_contacts, remove_contact_from_chat,
|
||||
send_text_msg,
|
||||
add_contact_to_chat, create_broadcast, create_group, get_chat_contacts,
|
||||
remove_contact_from_chat, send_text_msg, set_chat_name,
|
||||
};
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::stock_str::StockMessage;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::TestContextManager;
|
||||
@@ -492,7 +482,7 @@ mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_try_load() {
|
||||
async fn test_try_load() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let bob = &tcm.bob().await;
|
||||
let chat_id1 = create_group(bob, "a chat").await.unwrap();
|
||||
@@ -562,6 +552,15 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
|
||||
let chat_id = create_group(bob, "Δ-chat").await.unwrap();
|
||||
let chats = Chatlist::try_load(bob, 0, Some("δ"), None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert_eq!(chats.ids[0].0, chat_id);
|
||||
set_chat_name(bob, chat_id, "abcδe").await?;
|
||||
let chats = Chatlist::try_load(bob, 0, Some("Δ"), None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -599,6 +598,41 @@ mod tests {
|
||||
assert_eq!(chats.len(), 1);
|
||||
}
|
||||
|
||||
/// Test that DC_CHAT_TYPE_IN_BROADCAST are hidden
|
||||
/// and DC_CHAT_TYPE_OUT_BROADCAST are shown in chatlist for forwarding.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_broadcast_visiblity_on_forward() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_broadcast_a_id = create_broadcast(alice, "Channel Alice".to_string()).await?;
|
||||
let qr = get_securejoin_qr(alice, Some(alice_broadcast_a_id))
|
||||
.await
|
||||
.unwrap();
|
||||
let bob_broadcast_a_id = tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
||||
let bob_broadcast_b_id = create_broadcast(bob, "Channel Bob".to_string()).await?;
|
||||
|
||||
let chats = Chatlist::try_load(bob, DC_GCL_FOR_FORWARDING, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
!chats
|
||||
.iter()
|
||||
.any(|(chat_id, _)| chat_id == &bob_broadcast_a_id),
|
||||
"alice broadcast is not shown in bobs forwarding chatlist"
|
||||
);
|
||||
assert!(
|
||||
chats
|
||||
.iter()
|
||||
.any(|(chat_id, _)| chat_id == &bob_broadcast_b_id),
|
||||
"bobs own broadcast is shown in his forwarding chatlist"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_search_special_chat_names() {
|
||||
let t = TestContext::new_alice().await;
|
||||
@@ -807,6 +841,32 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_summary_prefix_for_channel() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
let alice_chat_id = create_broadcast(&alice, "alice's channel".to_string()).await?;
|
||||
let qr = get_securejoin_qr(&alice, Some(alice_chat_id)).await?;
|
||||
tcm.exec_securejoin_qr(&bob, &alice, &qr).await;
|
||||
|
||||
send_text_msg(&alice, alice_chat_id, "hi".into()).await?;
|
||||
let sent1 = alice.pop_sent_msg().await;
|
||||
let chatlist = Chatlist::try_load(&alice, 0, None, None).await?;
|
||||
let summary = chatlist.get_summary(&alice, 0, None).await?;
|
||||
assert!(summary.prefix.is_none());
|
||||
assert_eq!(summary.text, "hi");
|
||||
|
||||
bob.recv_msg(&sent1).await;
|
||||
let chatlist = Chatlist::try_load(&bob, 0, None, None).await?;
|
||||
let summary = chatlist.get_summary(&bob, 0, None).await?;
|
||||
assert!(summary.prefix.is_none());
|
||||
assert_eq!(summary.text, "hi");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_load_broken() {
|
||||
let t = TestContext::new_bob().await;
|
||||
|
||||
@@ -7,6 +7,7 @@ use colorutils_rs::{Oklch, Rgb, TransferFunction};
|
||||
use sha1::{Digest, Sha1};
|
||||
|
||||
/// Converts an identifier to Hue angle.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
fn str_to_angle(s: &str) -> f32 {
|
||||
let bytes = s.as_bytes();
|
||||
let result = Sha1::digest(bytes);
|
||||
@@ -19,6 +20,7 @@ fn str_to_angle(s: &str) -> f32 {
|
||||
///
|
||||
/// Returns a 24-bit number with 8 least significant bits corresponding to the blue color and 8
|
||||
/// most significant bits corresponding to the red color.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
fn rgb_to_u32(rgb: Rgb<u8>) -> u32 {
|
||||
65536 * u32::from(rgb.r) + 256 * u32::from(rgb.g) + u32::from(rgb.b)
|
||||
}
|
||||
|
||||
232
src/config.rs
232
src/config.rs
@@ -13,15 +13,14 @@ use strum_macros::{AsRefStr, Display, EnumIter, EnumString};
|
||||
use tokio::fs;
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::configure::EnteredLoginParam;
|
||||
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::provider::Provider;
|
||||
use crate::sync::{self, Sync::*, SyncData};
|
||||
use crate::tools::get_abs_path;
|
||||
use crate::tools::{get_abs_path, time};
|
||||
use crate::transport::{ConfiguredLoginParam, add_pseudo_transport, send_sync_transports};
|
||||
use crate::{constants, stats};
|
||||
|
||||
/// The available configuration keys.
|
||||
@@ -144,11 +143,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
|
||||
@@ -176,11 +175,6 @@ pub enum Config {
|
||||
#[strum(props(default = "0"))] // also change MediaQuality.default() on changes
|
||||
MediaQuality,
|
||||
|
||||
/// If set to "1", then existing messages are considered to be already fetched.
|
||||
/// This flag is reset after successful configuration.
|
||||
#[strum(props(default = "1"))]
|
||||
FetchedExistingMsgs,
|
||||
|
||||
/// Timer in seconds after which the message is deleted from the
|
||||
/// server.
|
||||
///
|
||||
@@ -200,11 +194,7 @@ pub enum Config {
|
||||
#[strum(props(default = "0"))]
|
||||
DeleteDeviceAfter,
|
||||
|
||||
/// Move messages to the Trash folder instead of marking them "\Deleted". Overrides
|
||||
/// `ProviderOptions::delete_to_trash`.
|
||||
DeleteToTrash,
|
||||
|
||||
/// The primary email address. Also see `SecondaryAddrs`.
|
||||
/// The primary email address.
|
||||
ConfiguredAddr,
|
||||
|
||||
/// List of configured IMAP servers as a JSON array.
|
||||
@@ -281,9 +271,6 @@ pub enum Config {
|
||||
/// Configured folder for chat messages.
|
||||
ConfiguredMvboxFolder,
|
||||
|
||||
/// Configured "Trash" folder.
|
||||
ConfiguredTrashFolder,
|
||||
|
||||
/// Unix timestamp of the last successful configuration.
|
||||
ConfiguredTimestamp,
|
||||
|
||||
@@ -306,10 +293,6 @@ pub enum Config {
|
||||
/// Meant to help profile owner to differ between profiles with similar names.
|
||||
PrivateTag,
|
||||
|
||||
/// All secondary self addresses separated by spaces
|
||||
/// (`addr1@example.org addr2@example.org addr3@example.org`)
|
||||
SecondaryAddrs,
|
||||
|
||||
/// Read-only core version string.
|
||||
#[strum(serialize = "sys.version")]
|
||||
SysVersion,
|
||||
@@ -345,10 +328,6 @@ pub enum Config {
|
||||
/// Timestamp of the last `CantDecryptOutgoingMsgs` notification.
|
||||
LastCantDecryptOutgoingMsgs,
|
||||
|
||||
/// To how many seconds to debounce scan_all_folders. Used mainly in tests, to disable debouncing completely.
|
||||
#[strum(props(default = "60"))]
|
||||
ScanAllFoldersDebounceSecs,
|
||||
|
||||
/// Whether to avoid using IMAP IDLE even if the server supports it.
|
||||
///
|
||||
/// This is a developer option for testing "fake idle".
|
||||
@@ -359,7 +338,17 @@ pub enum Config {
|
||||
DonationRequestNextCheck,
|
||||
|
||||
/// Defines the max. size (in bytes) of messages downloaded automatically.
|
||||
///
|
||||
/// For messages with large attachments, two messages are sent:
|
||||
/// a Pre-Message containing metadata and text and a Post-Message additionally
|
||||
/// containing the attachment. NB: Some "extra" metadata like avatars and gossiped
|
||||
/// encryption keys is stripped from post-messages to save traffic.
|
||||
/// Pre-Messages are shown as placeholder messages. They can be downloaded fully using
|
||||
/// `MsgId::download_full()` later. Post-Messages are automatically downloaded if they are
|
||||
/// smaller than the download_limit. Other messages are always auto-downloaded.
|
||||
///
|
||||
/// 0 = no limit.
|
||||
/// Changes only affect future messages.
|
||||
#[strum(props(default = "0"))]
|
||||
DownloadLimit,
|
||||
|
||||
@@ -438,8 +427,29 @@ pub enum Config {
|
||||
/// storing the same token multiple times on the server.
|
||||
EncryptedDeviceToken,
|
||||
|
||||
/// Return an error from `receive_imf_inner()` for a fully downloaded message. For tests.
|
||||
FailOnReceivingFullMsg,
|
||||
/// 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 tests.
|
||||
SimulateReceiveImfError,
|
||||
|
||||
/// Enable composing emails with Header Protection as defined in
|
||||
/// <https://www.rfc-editor.org/rfc/rfc9788.html> "Header Protection for Cryptographically
|
||||
/// Protected Email".
|
||||
#[strum(props(default = "1"))]
|
||||
StdHeaderProtectionComposing,
|
||||
|
||||
/// Who can call me.
|
||||
///
|
||||
/// The options are from the `WhoCanCallMe` enum.
|
||||
#[strum(props(default = "1"))]
|
||||
WhoCanCallMe,
|
||||
|
||||
/// Experimental option denoting that the current profile is shared between multiple team members.
|
||||
/// For now, the only effect of this option is that seen flags are not synchronized.
|
||||
TeamProfile,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -466,7 +476,10 @@ 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)
|
||||
matches!(
|
||||
self,
|
||||
Config::MvboxMove | Config::OnlyFetchMvbox | Config::ConfiguredAddr
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,7 +508,7 @@ impl Context {
|
||||
.into_owned()
|
||||
})
|
||||
}
|
||||
Config::SysVersion => Some((*constants::DC_VERSION_STR).clone()),
|
||||
Config::SysVersion => Some(constants::DC_VERSION_STR.to_string()),
|
||||
Config::SysMsgsizeMaxRecommended => Some(format!("{RECOMMENDED_FILE_SIZE}")),
|
||||
Config::SysConfigKeys => Some(get_config_keys_string()),
|
||||
_ => self.sql.get_raw_config(key.as_ref()).await?,
|
||||
@@ -512,10 +525,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 +591,7 @@ impl Context {
|
||||
.get_config(key)
|
||||
.await?
|
||||
.and_then(|s| s.parse::<i32>().ok())
|
||||
.map(|x| x != 0)
|
||||
.unwrap_or_default())
|
||||
.is_some_and(|x| x != 0))
|
||||
}
|
||||
|
||||
/// Returns true if movebox ("DeltaChat" folder) should be watched.
|
||||
@@ -600,12 +608,6 @@ impl Context {
|
||||
&& !self.get_config_bool(Config::Bot).await?)
|
||||
}
|
||||
|
||||
/// Returns whether sync messages should be uploaded to the mvbox.
|
||||
pub(crate) async fn should_move_sync_msgs(&self) -> Result<bool> {
|
||||
Ok(self.get_config_bool(Config::MvboxMove).await?
|
||||
|| !self.get_config_bool(Config::IsChatmail).await?)
|
||||
}
|
||||
|
||||
/// Returns whether MDNs should be requested.
|
||||
pub(crate) async fn should_request_mdns(&self) -> Result<bool> {
|
||||
match self.get_config_bool_opt(Config::MdnsEnabled).await? {
|
||||
@@ -636,15 +638,14 @@ impl Context {
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
/// Gets the configured provider, as saved in the `configured_provider` value.
|
||||
/// Gets the configured provider.
|
||||
///
|
||||
/// The provider is determined by `get_provider_info()` during configuration and then saved
|
||||
/// to the db in `param.save_to_database()`, together with all the other `configured_*` values.
|
||||
/// The provider is determined by the current primary transport.
|
||||
pub async fn get_configured_provider(&self) -> Result<Option<&'static Provider>> {
|
||||
if let Some(cfg) = self.get_config(Config::ConfiguredProvider).await? {
|
||||
return Ok(get_provider_by_id(&cfg));
|
||||
}
|
||||
Ok(None)
|
||||
let provider = ConfiguredLoginParam::load(self)
|
||||
.await?
|
||||
.and_then(|(_transport_id, param)| param.provider);
|
||||
Ok(provider)
|
||||
}
|
||||
|
||||
/// Gets configured "delete_device_after" value.
|
||||
@@ -665,7 +666,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),
|
||||
};
|
||||
@@ -683,7 +684,6 @@ impl Context {
|
||||
| Config::MdnsEnabled
|
||||
| Config::MvboxMove
|
||||
| Config::OnlyFetchMvbox
|
||||
| Config::DeleteToTrash
|
||||
| Config::Configured
|
||||
| Config::Bot
|
||||
| Config::NotifyAboutWrongPw
|
||||
@@ -706,6 +706,11 @@ impl Context {
|
||||
pub async fn set_config(&self, key: Config, value: Option<&str>) -> Result<()> {
|
||||
Self::check_config(key, value)?;
|
||||
|
||||
let n_transports = self.count_transports().await?;
|
||||
if n_transports > 1 && matches!(key, Config::MvboxMove | Config::OnlyFetchMvbox) {
|
||||
bail!("Cannot reconfigure {key} when multiple transports are configured");
|
||||
}
|
||||
|
||||
let _pause = match key.needs_io_restart() {
|
||||
true => self.scheduler.pause(self).await?,
|
||||
_ => Default::default(),
|
||||
@@ -791,19 +796,67 @@ impl Context {
|
||||
.await?;
|
||||
}
|
||||
Config::ConfiguredAddr => {
|
||||
if self.is_configured().await? {
|
||||
bail!("Cannot change ConfiguredAddr");
|
||||
}
|
||||
if let Some(addr) = value {
|
||||
let Some(addr) = value else {
|
||||
bail!("Cannot unset configured_addr");
|
||||
};
|
||||
|
||||
if !self.is_configured().await? {
|
||||
info!(
|
||||
self,
|
||||
"Creating a pseudo configured account which will not be able to send or receive messages. Only meant for tests!"
|
||||
);
|
||||
ConfiguredLoginParam::from_json(&format!(
|
||||
r#"{{"addr":"{addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#
|
||||
))?
|
||||
.save_to_transports_table(self, &EnteredLoginParam::default())
|
||||
.await?;
|
||||
add_pseudo_transport(self, addr).await?;
|
||||
self.sql
|
||||
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(addr))
|
||||
.await?;
|
||||
} else {
|
||||
self.sql
|
||||
.transaction(|transaction| {
|
||||
if transaction.query_row(
|
||||
"SELECT COUNT(*) FROM transports WHERE addr=?",
|
||||
(addr,),
|
||||
|row| {
|
||||
let res: i64 = row.get(0)?;
|
||||
Ok(res)
|
||||
},
|
||||
)? == 0
|
||||
{
|
||||
bail!("Address does not belong to any transport.");
|
||||
}
|
||||
transaction.execute(
|
||||
"UPDATE config SET value=? WHERE keyname='configured_addr'",
|
||||
(addr,),
|
||||
)?;
|
||||
|
||||
// Update the timestamp for the primary transport
|
||||
// so it becomes the first in `get_all_self_addrs()` list
|
||||
// and the list of relays distributed in the public key.
|
||||
// This ensures that messages will be sent
|
||||
// to the primary relay by the contacts
|
||||
// and will be fetched in background_fetch()
|
||||
// which only fetches from the primary transport.
|
||||
transaction
|
||||
.execute(
|
||||
"UPDATE transports SET add_timestamp=? WHERE addr=?",
|
||||
(time(), addr),
|
||||
)
|
||||
.context(
|
||||
"Failed to update add_timestamp for the new primary transport",
|
||||
)?;
|
||||
|
||||
// Clean up SMTP and IMAP APPEND queue.
|
||||
//
|
||||
// The messages in the queue have a different
|
||||
// From address so we cannot send them over
|
||||
// the new SMTP transport.
|
||||
transaction.execute("DELETE FROM smtp", ())?;
|
||||
transaction.execute("DELETE FROM imap_send", ())?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
send_sync_transports(self).await?;
|
||||
self.sql.uncache_raw_config("configured_addr").await;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
@@ -834,7 +887,7 @@ impl Context {
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
self.scheduler.interrupt_inbox().await;
|
||||
self.scheduler.interrupt_smtp().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -898,17 +951,7 @@ impl Context {
|
||||
/// This should only be used by test code and during configure.
|
||||
#[cfg(test)] // AEAP is disabled, but there are still tests for it
|
||||
pub(crate) async fn set_primary_self_addr(&self, primary_new: &str) -> Result<()> {
|
||||
self.quota.write().await.take();
|
||||
|
||||
// add old primary address (if exists) to secondary addresses
|
||||
let mut secondary_addrs = self.get_all_self_addrs().await?;
|
||||
// never store a primary address also as a secondary
|
||||
secondary_addrs.retain(|a| !addr_cmp(a, primary_new));
|
||||
self.set_config_internal(
|
||||
Config::SecondaryAddrs,
|
||||
Some(secondary_addrs.join(" ").as_str()),
|
||||
)
|
||||
.await?;
|
||||
self.quota.write().await.clear();
|
||||
|
||||
self.sql
|
||||
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(primary_new))
|
||||
@@ -917,24 +960,26 @@ impl Context {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns all primary and secondary self addresses.
|
||||
/// Returns all self addresses, newest first.
|
||||
pub(crate) async fn get_all_self_addrs(&self) -> Result<Vec<String>> {
|
||||
let primary_addrs = self.get_config(Config::ConfiguredAddr).await?.into_iter();
|
||||
let secondary_addrs = self.get_secondary_self_addrs().await?.into_iter();
|
||||
|
||||
Ok(primary_addrs.chain(secondary_addrs).collect())
|
||||
self.sql
|
||||
.query_map_vec(
|
||||
"SELECT addr FROM transports ORDER BY add_timestamp DESC",
|
||||
(),
|
||||
|row| {
|
||||
let addr: String = row.get(0)?;
|
||||
Ok(addr)
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns all secondary self addresses.
|
||||
pub(crate) async fn get_secondary_self_addrs(&self) -> Result<Vec<String>> {
|
||||
let secondary_addrs = self
|
||||
.get_config(Config::SecondaryAddrs)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
Ok(secondary_addrs
|
||||
.split_ascii_whitespace()
|
||||
.map(|s| s.to_string())
|
||||
.collect())
|
||||
self.sql.query_map_vec("SELECT addr FROM transports WHERE addr NOT IN (SELECT value FROM config WHERE keyname='configured_addr')", (), |row| {
|
||||
let addr: String = row.get(0)?;
|
||||
Ok(addr)
|
||||
}).await
|
||||
}
|
||||
|
||||
/// Returns the primary self address.
|
||||
@@ -957,5 +1002,18 @@ fn get_config_keys_string() -> String {
|
||||
format!(" {keys} ")
|
||||
}
|
||||
|
||||
/// Returns all `ui.*` config keys that were set by the UI.
|
||||
pub async fn get_all_ui_config_keys(context: &Context) -> Result<Vec<String>> {
|
||||
let ui_keys = context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT keyname FROM config WHERE keyname GLOB 'ui.*' ORDER BY config.id",
|
||||
(),
|
||||
|row| Ok(row.get::<_, String>(0)?),
|
||||
)
|
||||
.await?;
|
||||
Ok(ui_keys)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod config_tests;
|
||||
|
||||
@@ -81,6 +81,37 @@ async fn test_ui_config() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_all_ui_config_keys() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
t.set_ui_config("ui.android.screen_security", Some("safe"))
|
||||
.await?;
|
||||
t.set_ui_config("ui.lastchatid", Some("231")).await?;
|
||||
t.set_ui_config(
|
||||
"ui.desktop.webxdcBounds.528490",
|
||||
Some(r#"{"x":954,"y":356,"width":378,"height":671}"#),
|
||||
)
|
||||
.await?;
|
||||
t.set_ui_config(
|
||||
"ui.desktop.webxdcBounds.556543",
|
||||
Some(r#"{"x":954,"y":356,"width":378,"height":671}"#),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
get_all_ui_config_keys(&t).await?,
|
||||
vec![
|
||||
"ui.android.screen_security",
|
||||
"ui.lastchatid",
|
||||
"ui.desktop.webxdcBounds.528490",
|
||||
"ui.desktop.webxdcBounds.556543"
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Regression test for https://github.com/deltachat/deltachat-core-rust/issues/3012
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_config_bool() -> Result<()> {
|
||||
@@ -94,59 +125,6 @@ async fn test_set_config_bool() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_self_addrs() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
assert!(alice.is_self_addr("alice@example.org").await?);
|
||||
assert_eq!(alice.get_all_self_addrs().await?, vec!["alice@example.org"]);
|
||||
assert!(!alice.is_self_addr("alice@alice.com").await?);
|
||||
|
||||
// Test adding the same primary address
|
||||
alice.set_primary_self_addr("alice@example.org").await?;
|
||||
alice.set_primary_self_addr("Alice@Example.Org").await?;
|
||||
assert_eq!(alice.get_all_self_addrs().await?, vec!["Alice@Example.Org"]);
|
||||
|
||||
// Test adding a new (primary) self address
|
||||
// The address is trimmed during configure by `LoginParam::from_database()`,
|
||||
// so `set_primary_self_addr()` doesn't have to trim it.
|
||||
alice.set_primary_self_addr("Alice@alice.com").await?;
|
||||
assert!(alice.is_self_addr("aliCe@example.org").await?);
|
||||
assert!(alice.is_self_addr("alice@alice.com").await?);
|
||||
assert_eq!(
|
||||
alice.get_all_self_addrs().await?,
|
||||
vec!["Alice@alice.com", "Alice@Example.Org"]
|
||||
);
|
||||
|
||||
// Check that the entry is not duplicated
|
||||
alice.set_primary_self_addr("alice@alice.com").await?;
|
||||
alice.set_primary_self_addr("alice@alice.com").await?;
|
||||
assert_eq!(
|
||||
alice.get_all_self_addrs().await?,
|
||||
vec!["alice@alice.com", "Alice@Example.Org"]
|
||||
);
|
||||
|
||||
// Test switching back
|
||||
alice.set_primary_self_addr("alice@example.org").await?;
|
||||
assert_eq!(
|
||||
alice.get_all_self_addrs().await?,
|
||||
vec!["alice@example.org", "alice@alice.com"]
|
||||
);
|
||||
|
||||
// Test setting a new primary self address, the previous self address
|
||||
// should be kept as a secondary self address
|
||||
alice.set_primary_self_addr("alice@alice.xyz").await?;
|
||||
assert_eq!(
|
||||
alice.get_all_self_addrs().await?,
|
||||
vec!["alice@alice.xyz", "alice@example.org", "alice@alice.com"]
|
||||
);
|
||||
assert!(alice.is_self_addr("alice@example.org").await?);
|
||||
assert!(alice.is_self_addr("alice@alice.com").await?);
|
||||
assert!(alice.is_self_addr("Alice@alice.xyz").await?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_mdns_default_behaviour() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
@@ -278,7 +256,6 @@ async fn test_sync() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sync message mustn't be sent if self-{status,avatar} is changed by a self-sent message.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_sync_on_self_sent_msg() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -288,16 +265,16 @@ async fn test_no_sync_on_self_sent_msg() -> Result<()> {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
|
||||
let status = "Synced via usual message";
|
||||
let status = "Sent via usual message";
|
||||
alice0.set_config(Config::Selfstatus, Some(status)).await?;
|
||||
alice0.send_sync_msg().await?;
|
||||
alice0.pop_sent_sync_msg().await;
|
||||
alice0.pop_sent_msg().await;
|
||||
let status1 = "Synced via sync message";
|
||||
alice1.set_config(Config::Selfstatus, Some(status1)).await?;
|
||||
tcm.send_recv(alice0, alice1, "hi Alice!").await;
|
||||
assert_eq!(
|
||||
alice1.get_config(Config::Selfstatus).await?,
|
||||
Some(status.to_string())
|
||||
Some(status1.to_string())
|
||||
);
|
||||
sync(alice1, alice0).await;
|
||||
assert_eq!(
|
||||
@@ -315,7 +292,7 @@ async fn test_no_sync_on_self_sent_msg() -> Result<()> {
|
||||
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
|
||||
.await?;
|
||||
alice0.send_sync_msg().await?;
|
||||
alice0.pop_sent_sync_msg().await;
|
||||
alice0.pop_sent_msg().await;
|
||||
let file = alice1.dir.path().join("avatar.jpg");
|
||||
let bytes = include_bytes!("../../test-data/image/avatar1000x1000.jpg");
|
||||
tokio::fs::write(&file, bytes).await?;
|
||||
@@ -328,7 +305,7 @@ async fn test_no_sync_on_self_sent_msg() -> Result<()> {
|
||||
alice1
|
||||
.get_config(Config::Selfavatar)
|
||||
.await?
|
||||
.filter(|path| path.ends_with(".png"))
|
||||
.filter(|path| path.ends_with(".jpg"))
|
||||
.is_some()
|
||||
);
|
||||
sync(alice1, alice0).await;
|
||||
|
||||
260
src/configure.rs
260
src/configure.rs
@@ -23,26 +23,31 @@ use percent_encoding::utf8_percent_encode;
|
||||
use server_params::{ServerParams, expand_param_vector};
|
||||
use tokio::task;
|
||||
|
||||
use crate::config::{self, Config};
|
||||
use crate::config::Config;
|
||||
use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT;
|
||||
use crate::context::Context;
|
||||
use crate::imap::Imap;
|
||||
use crate::log::{LogExt, info, warn};
|
||||
use crate::log::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, send_sync_transports,
|
||||
};
|
||||
use crate::{EventType, stock_str};
|
||||
use crate::{chat, provider};
|
||||
use deltachat_contact_tools::addr_cmp;
|
||||
|
||||
/// Maximum number of relays
|
||||
/// see <https://github.com/chatmail/core/issues/7608>
|
||||
pub(crate) const MAX_TRANSPORT_RELAYS: usize = 5;
|
||||
|
||||
macro_rules! progress {
|
||||
($context:tt, $progress:expr, $comment:expr) => {
|
||||
@@ -128,12 +133,6 @@ impl Context {
|
||||
"cannot configure, database not opened."
|
||||
);
|
||||
param.addr = addr_normalize(¶m.addr);
|
||||
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
|
||||
if self.is_configured().await? && !addr_cmp(&old_addr.unwrap_or_default(), ¶m.addr) {
|
||||
let error_msg = "Changing your email address is not supported right now. Check back in a few months!";
|
||||
progress!(self, 0, Some(error_msg.to_string()));
|
||||
bail!(error_msg);
|
||||
}
|
||||
let cancel_channel = self.alloc_ongoing().await?;
|
||||
|
||||
let res = self
|
||||
@@ -192,38 +191,134 @@ 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)
|
||||
}
|
||||
|
||||
/// Returns the number of configured transports.
|
||||
pub async fn count_transports(&self) -> Result<usize> {
|
||||
self.sql.count("SELECT COUNT(*) FROM transports", ()).await
|
||||
}
|
||||
|
||||
/// Removes the transport with the specified email address
|
||||
/// (i.e. [EnteredLoginParam::addr]).
|
||||
#[expect(clippy::unused_async)]
|
||||
pub async fn delete_transport(&self, _addr: &str) -> Result<()> {
|
||||
bail!(
|
||||
"Adding and removing additional transports is not supported yet. Check back in a few months!"
|
||||
)
|
||||
pub async fn delete_transport(&self, addr: &str) -> Result<()> {
|
||||
let now = time();
|
||||
let removed_transport_id = self
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let primary_addr = transaction.query_row(
|
||||
"SELECT value FROM config WHERE keyname='configured_addr'",
|
||||
(),
|
||||
|row| {
|
||||
let addr: String = row.get(0)?;
|
||||
Ok(addr)
|
||||
},
|
||||
)?;
|
||||
|
||||
if primary_addr == addr {
|
||||
bail!("Cannot delete primary transport");
|
||||
}
|
||||
let (transport_id, add_timestamp) = transaction.query_row(
|
||||
"DELETE FROM transports WHERE addr=? RETURNING id, add_timestamp",
|
||||
(addr,),
|
||||
|row| {
|
||||
let id: u32 = row.get(0)?;
|
||||
let add_timestamp: i64 = row.get(1)?;
|
||||
Ok((id, add_timestamp))
|
||||
},
|
||||
)?;
|
||||
transaction.execute("DELETE FROM imap WHERE transport_id=?", (transport_id,))?;
|
||||
transaction.execute(
|
||||
"DELETE FROM imap_sync WHERE transport_id=?",
|
||||
(transport_id,),
|
||||
)?;
|
||||
|
||||
// Removal timestamp should not be lower than addition timestamp
|
||||
// to be accepted by other devices when synced.
|
||||
let remove_timestamp = std::cmp::max(now, add_timestamp);
|
||||
|
||||
transaction.execute(
|
||||
"INSERT INTO removed_transports (addr, remove_timestamp)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT (addr)
|
||||
DO UPDATE SET remove_timestamp = excluded.remove_timestamp",
|
||||
(addr, remove_timestamp),
|
||||
)?;
|
||||
|
||||
Ok(transport_id)
|
||||
})
|
||||
.await?;
|
||||
send_sync_transports(self).await?;
|
||||
self.quota.write().await.remove(&removed_transport_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn inner_configure(&self, param: &EnteredLoginParam) -> Result<()> {
|
||||
info!(self, "Configure ...");
|
||||
|
||||
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
|
||||
let provider = configure(self, param).await?;
|
||||
if old_addr.is_some()
|
||||
&& !self
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM transports WHERE addr=?",
|
||||
(¶m.addr,),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
// Should be checked before `MvboxMove` because the latter makes no sense in presense of
|
||||
// `OnlyFetchMvbox` and even grayed out in the UIs in this case.
|
||||
if self.get_config(Config::OnlyFetchMvbox).await?.as_deref() != Some("0") {
|
||||
bail!(
|
||||
"To use additional relays, disable the legacy option \"Settings / Advanced / Only Fetch from DeltaChat Folder\"."
|
||||
);
|
||||
}
|
||||
if self.get_config(Config::MvboxMove).await?.as_deref() != Some("0") {
|
||||
bail!(
|
||||
"To use additional relays, disable the legacy option \"Settings / Advanced / Move automatically to DeltaChat Folder\"."
|
||||
);
|
||||
}
|
||||
|
||||
if self
|
||||
.sql
|
||||
.count("SELECT COUNT(*) FROM transports", ())
|
||||
.await?
|
||||
>= MAX_TRANSPORT_RELAYS
|
||||
{
|
||||
bail!(
|
||||
"You have reached the maximum number of relays ({}).",
|
||||
MAX_TRANSPORT_RELAYS
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let provider = match configure(self, param).await {
|
||||
Err(error) => {
|
||||
// Log entered and actual params
|
||||
let configured_param = get_configured_param(self, param).await;
|
||||
warn!(
|
||||
self,
|
||||
"configure failed: Entered params: {}. Used params: {}. Error: {error}.",
|
||||
param.to_string(),
|
||||
configured_param
|
||||
.map(|param| param.to_string())
|
||||
.unwrap_or("error".to_owned())
|
||||
);
|
||||
return Err(error);
|
||||
}
|
||||
Ok(provider) => provider,
|
||||
};
|
||||
self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
|
||||
.await?;
|
||||
on_configure_completed(self, provider, old_addr).await?;
|
||||
on_configure_completed(self, provider).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -231,7 +326,6 @@ impl Context {
|
||||
async fn on_configure_completed(
|
||||
context: &Context,
|
||||
provider: Option<&'static Provider>,
|
||||
old_addr: Option<String>,
|
||||
) -> Result<()> {
|
||||
if let Some(provider) = provider {
|
||||
if let Some(config_defaults) = provider.config_defaults {
|
||||
@@ -261,21 +355,6 @@ 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -470,9 +549,6 @@ async fn get_configured_param(
|
||||
async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'static Provider>> {
|
||||
progress!(ctx, 1);
|
||||
|
||||
let ctx2 = ctx.clone();
|
||||
let update_device_chats_handle = task::spawn(async move { ctx2.update_device_chats().await });
|
||||
|
||||
let configured_param = get_configured_param(ctx, param).await?;
|
||||
let proxy_config = ProxyConfig::load(ctx).await?;
|
||||
let strict_tls = configured_param.strict_tls(proxy_config.is_some());
|
||||
@@ -507,91 +583,65 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
|
||||
|
||||
// Configure IMAP
|
||||
|
||||
let transport_id = 0;
|
||||
let (_s, r) = async_channel::bounded(1);
|
||||
let mut imap = Imap::new(
|
||||
configured_param.imap.clone(),
|
||||
configured_param.imap_password.clone(),
|
||||
proxy_config,
|
||||
&configured_param.addr,
|
||||
strict_tls,
|
||||
configured_param.oauth2,
|
||||
r,
|
||||
);
|
||||
let mut imap = Imap::new(ctx, transport_id, configured_param.clone(), r).await?;
|
||||
let configuring = true;
|
||||
let mut imap_session = match imap.connect(ctx, configuring).await {
|
||||
Ok(session) => session,
|
||||
Err(err) => bail!(
|
||||
"{}",
|
||||
nicer_configuration_error(ctx, format!("{err:#}")).await
|
||||
),
|
||||
let imap_session = match imap.connect(ctx, configuring).await {
|
||||
Ok(imap_session) => imap_session,
|
||||
Err(err) => {
|
||||
bail!(
|
||||
"{}",
|
||||
nicer_configuration_error(ctx, format!("{err:#}")).await
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
progress!(ctx, 850);
|
||||
|
||||
// Wait for SMTP configuration
|
||||
smtp_config_task.await.unwrap()?;
|
||||
smtp_config_task.await??;
|
||||
|
||||
progress!(ctx, 900);
|
||||
|
||||
let is_chatmail = match ctx.get_config_bool(Config::FixIsChatmail).await? {
|
||||
false => {
|
||||
let is_chatmail = imap_session.is_chatmail();
|
||||
ctx.set_config(
|
||||
Config::IsChatmail,
|
||||
Some(match is_chatmail {
|
||||
false => "0",
|
||||
true => "1",
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
is_chatmail
|
||||
let is_configured = ctx.is_configured().await?;
|
||||
if !is_configured {
|
||||
ctx.sql.set_raw_config("mvbox_move", Some("0")).await?;
|
||||
ctx.sql.set_raw_config("only_fetch_mvbox", None).await?;
|
||||
}
|
||||
if !ctx.get_config_bool(Config::FixIsChatmail).await? {
|
||||
if imap_session.is_chatmail() {
|
||||
ctx.sql.set_raw_config("is_chatmail", Some("1")).await?;
|
||||
} else if !is_configured {
|
||||
// Reset the setting that may have been set
|
||||
// during failed configuration.
|
||||
ctx.sql.set_raw_config("is_chatmail", Some("0")).await?;
|
||||
}
|
||||
true => ctx.get_config_bool(Config::IsChatmail).await?,
|
||||
};
|
||||
if is_chatmail {
|
||||
ctx.set_config(Config::MvboxMove, Some("0")).await?;
|
||||
ctx.set_config(Config::OnlyFetchMvbox, None).await?;
|
||||
ctx.set_config(Config::ShowEmails, None).await?;
|
||||
}
|
||||
|
||||
let create_mvbox = !is_chatmail;
|
||||
imap.configure_folders(ctx, &mut imap_session, create_mvbox)
|
||||
.await?;
|
||||
|
||||
let create = true;
|
||||
imap_session
|
||||
.select_with_uidvalidity(ctx, "INBOX", create)
|
||||
.await
|
||||
.context("could not read INBOX status")?;
|
||||
|
||||
drop(imap_session);
|
||||
drop(imap);
|
||||
|
||||
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)
|
||||
.clone()
|
||||
.save_to_transports_table(ctx, param, time())
|
||||
.await?;
|
||||
send_sync_transports(ctx).await?;
|
||||
|
||||
ctx.set_config_internal(Config::ConfiguredTimestamp, Some(&time().to_string()))
|
||||
.await?;
|
||||
|
||||
progress!(ctx, 920);
|
||||
|
||||
ctx.set_config_internal(Config::FetchedExistingMsgs, config::from_bool(false))
|
||||
.await?;
|
||||
ctx.scheduler.interrupt_inbox().await;
|
||||
|
||||
progress!(ctx, 940);
|
||||
update_device_chats_handle.await??;
|
||||
ctx.update_device_chats()
|
||||
.await
|
||||
.context("Failed to update device chats")?;
|
||||
|
||||
ctx.sql.set_raw_config_bool("configured", true).await?;
|
||||
ctx.emit_event(EventType::AccountsItemChanged);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user