mirror of
https://github.com/chatmail/core.git
synced 2026-10-03 19:50:33 +03:00
Compare commits
40
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8468f5b5dd | ||
|
|
dbba555a63 | ||
|
|
8cedea1958 | ||
|
|
70730490b2 | ||
|
|
9578c2c054 | ||
|
|
9e2d668490 | ||
|
|
d18718d2b9 | ||
|
|
77bb77c33e | ||
|
|
1143317fd1 | ||
|
|
83211e440e | ||
|
|
7cdfb234a3 | ||
|
|
2a9bc732bf | ||
|
|
90387fdbea | ||
|
|
29006e47c5 | ||
|
|
79bf0c79a5 | ||
|
|
e5d1bf7504 | ||
|
|
32d40cbb14 | ||
|
|
4e83357371 | ||
|
|
88deea5b18 | ||
|
|
8883e5ada0 | ||
|
|
ca2a40e210 | ||
|
|
1cd2957a4d | ||
|
|
4d57b8bf32 | ||
|
|
e203ae9571 | ||
|
|
a37899b365 | ||
|
|
3173487c9f | ||
|
|
aade549ccc | ||
|
|
736979b451 | ||
|
|
5a882b0aca | ||
|
|
1e36fb74be | ||
|
|
ebf1c87483 | ||
|
|
172ebc08f9 | ||
|
|
eeb9337a56 | ||
|
|
a1eb593d81 | ||
|
|
7e070efc28 | ||
|
|
5d3145d165 | ||
|
|
1605b971ba | ||
|
|
22578ea4b6 | ||
|
|
076f83f320 | ||
|
|
8ab98019a7 |
@@ -20,7 +20,7 @@ permissions: {}
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
RUST_VERSION: 1.98.1
|
||||
RUST_VERSION: 1.99.0
|
||||
|
||||
# Minimum Supported Rust Version
|
||||
MSRV: 1.89.0
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
cache-bin: false
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@b6ff580856c41316412a0b9b60540fbc6f8c82cc
|
||||
uses: taiki-e/install-action@9983c65e42da123ff25d1f78505eb6de315aa172
|
||||
with:
|
||||
tool: nextest
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-wheel
|
||||
@@ -157,7 +157,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
|
||||
@@ -181,7 +181,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android-wheel
|
||||
@@ -208,7 +208,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
|
||||
- name: Download Linux aarch64 binary
|
||||
uses: actions/download-artifact@v7
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
- run: nix fmt flake.nix nix/ -- --check
|
||||
|
||||
build:
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
build-macos:
|
||||
@@ -106,5 +106,5 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
- name: Build
|
||||
run: nix build .#deltachat-repl-win64
|
||||
- name: Upload binary
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
- name: Build Python documentation
|
||||
run: nix build .#python-docs
|
||||
- name: Upload to py.delta.chat
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
- name: Build C documentation
|
||||
run: nix build .#docs
|
||||
- name: Upload to c.delta.chat
|
||||
|
||||
@@ -23,4 +23,4 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
|
||||
uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
|
||||
|
||||
Generated
+118
-149
@@ -194,9 +194,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "astral-tokio-tar"
|
||||
version = "0.6.4"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b18457efd137254e016bbde5e1d88df61c4e1a5ae2223746e56123bac6af2463"
|
||||
checksum = "6f2e989b33246fe9240d39accf4dd9a01e0b6c1f3ce9dd095e0a47fa02505523"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"libc",
|
||||
@@ -205,6 +205,7 @@ dependencies = [
|
||||
"rustix 1.1.4",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"zerocopy 0.8.59",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -310,7 +311,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37dd6b179962fe4048a6f81d4c0d7ed419a21fdf49204b4c6b04971693358e79"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
@@ -327,7 +328,7 @@ dependencies = [
|
||||
"log",
|
||||
"nom 8.0.0",
|
||||
"pin-project",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -363,7 +364,7 @@ dependencies = [
|
||||
"crc32fast",
|
||||
"futures-lite",
|
||||
"pin-project",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
@@ -459,7 +460,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1386,7 +1387,6 @@ dependencies = [
|
||||
"regex",
|
||||
"rusqlite",
|
||||
"sanitize-filename",
|
||||
"sdp",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha-1",
|
||||
@@ -1399,7 +1399,7 @@ dependencies = [
|
||||
"tempfile",
|
||||
"testdir",
|
||||
"textwrap",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
"tokio-io-timeout",
|
||||
"tokio-rustls",
|
||||
@@ -1503,7 +1503,7 @@ name = "deltachat_derive"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 3.0.4",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1518,7 +1518,7 @@ dependencies = [
|
||||
"num-traits",
|
||||
"rand 0.9.4",
|
||||
"serde_json",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
"yerpc",
|
||||
]
|
||||
@@ -1677,9 +1677,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "6.0.0"
|
||||
version = "7.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
||||
checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
]
|
||||
@@ -2091,12 +2091,6 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
@@ -2228,7 +2222,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.4",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2417,34 +2411,16 @@ checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash 0.1.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
dependencies = [
|
||||
"foldhash 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
dependencies = [
|
||||
"foldhash 0.2.0",
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.12.2"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a596f1b20ed2cc5ecac41a164aaebc7258057060f06c0cf7a2ba3991ee7990fb"
|
||||
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
|
||||
dependencies = [
|
||||
"hashbrown 0.17.1",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2489,7 +2465,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"rand 0.9.4",
|
||||
"ring",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"tinyvec",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -2512,7 +2488,7 @@ dependencies = [
|
||||
"rand 0.9.4",
|
||||
"resolv-conf",
|
||||
"smallvec",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
@@ -2678,9 +2654,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.10.1"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
|
||||
checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
@@ -2718,15 +2694,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.20"
|
||||
version = "0.1.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||
checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"http 1.1.0",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"hyper",
|
||||
"libc",
|
||||
"pin-project-lite",
|
||||
@@ -2978,7 +2955,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.15.4",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3069,7 +3046,7 @@ dependencies = [
|
||||
"strum 0.26.2",
|
||||
"stun-rs",
|
||||
"surge-ping",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
@@ -3094,7 +3071,7 @@ dependencies = [
|
||||
"ed25519-dalek",
|
||||
"rand_core 0.6.4",
|
||||
"serde",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"url",
|
||||
]
|
||||
|
||||
@@ -3136,7 +3113,7 @@ dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
"serde",
|
||||
"serde-error",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
@@ -3181,7 +3158,7 @@ dependencies = [
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2 0.5.9",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
@@ -3201,7 +3178,7 @@ dependencies = [
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
@@ -3256,7 +3233,7 @@ dependencies = [
|
||||
"sha1",
|
||||
"strum 0.26.2",
|
||||
"stun-rs",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
@@ -3285,12 +3262,11 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.105"
|
||||
version = "0.3.77"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e"
|
||||
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
@@ -3369,11 +3345,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.38.2"
|
||||
version = "0.35.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1d20bef17f513b9b3004532233187769cd072d790971f4e4da0e346eb6401e8"
|
||||
checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"openssl-sys",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
@@ -3445,7 +3422,7 @@ version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.4",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3474,9 +3451,9 @@ checksum = "4c942e8a4b83f9351236c1e531ea9fa0237913d63c7fc36818430e0128a1ddf3"
|
||||
|
||||
[[package]]
|
||||
name = "mailparse"
|
||||
version = "0.16.1"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60819a97ddcb831a5614eb3b0174f3620e793e97e09195a395bfa948fd68ed2f"
|
||||
checksum = "8469955699e572455f8e1a40689e07ab309911d659632f65062598192254c091"
|
||||
dependencies = [
|
||||
"charset",
|
||||
"data-encoding",
|
||||
@@ -3741,7 +3718,7 @@ dependencies = [
|
||||
"log",
|
||||
"netlink-packet-core",
|
||||
"netlink-sys",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4230,7 +4207,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"ucd-trie",
|
||||
]
|
||||
|
||||
@@ -4406,7 +4383,7 @@ dependencies = [
|
||||
"serde",
|
||||
"sha1_smol",
|
||||
"simple-dns",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"url",
|
||||
@@ -4619,7 +4596,7 @@ version = "0.2.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
"zerocopy 0.7.35",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4804,7 +4781,7 @@ dependencies = [
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2 0.5.9",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
@@ -4825,7 +4802,7 @@ dependencies = [
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
@@ -5064,7 +5041,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
|
||||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"libredox",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5234,21 +5211,11 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rsqlite-vfs"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c"
|
||||
dependencies = [
|
||||
"hashbrown 0.16.1",
|
||||
"thiserror 2.0.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.40.2"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23f2a97da3e3873c73cb2a2e71b35c40ff95e0b1eefa8d72d8499a6928c3b5b3"
|
||||
checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"fallible-iterator",
|
||||
@@ -5256,7 +5223,6 @@ dependencies = [
|
||||
"hashlink",
|
||||
"libsqlite3-sys",
|
||||
"smallvec",
|
||||
"sqlite-wasm-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5479,18 +5445,6 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "sdp"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22c3b0257608d7de4de4c4ea650ccc2e6e3e45e3cd80039fcdee768bcb449253"
|
||||
dependencies = [
|
||||
"rand 0.9.4",
|
||||
"substring",
|
||||
"thiserror 1.0.69",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sec1"
|
||||
version = "0.7.3"
|
||||
@@ -5596,7 +5550,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.4",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5752,7 +5706,7 @@ dependencies = [
|
||||
"shadowsocks-crypto",
|
||||
"socket2 0.5.9",
|
||||
"spin 0.10.1",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
"tokio-tfo",
|
||||
"trait-variant",
|
||||
@@ -5859,14 +5813,14 @@ dependencies = [
|
||||
"sha3",
|
||||
"signature",
|
||||
"typenum",
|
||||
"zerocopy",
|
||||
"zerocopy 0.7.35",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.2"
|
||||
version = "1.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||
checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891"
|
||||
|
||||
[[package]]
|
||||
name = "smawk"
|
||||
@@ -5961,18 +5915,6 @@ dependencies = [
|
||||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlite-wasm-rs"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"js-sys",
|
||||
"rsqlite-vfs",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
@@ -6061,15 +6003,6 @@ dependencies = [
|
||||
"rand 0.9.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "substring"
|
||||
version = "1.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42ee6433ecef213b2e72f587ef64a2f5943e7cd16fbd82dbe8bc07486c534c86"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
@@ -6116,9 +6049,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.4"
|
||||
version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
|
||||
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -6262,11 +6195,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.20"
|
||||
version = "2.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
|
||||
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.20",
|
||||
"thiserror-impl 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6282,13 +6215,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.20"
|
||||
version = "2.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
|
||||
checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.4",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6408,9 +6341,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.4"
|
||||
version = "0.26.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
||||
checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"tokio",
|
||||
@@ -6810,9 +6743,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.25.0"
|
||||
version = "1.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f053576934f05a761a402421fbbe3d425d9366f75f978806a037b3ca481abecc"
|
||||
checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce"
|
||||
dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"js-sys",
|
||||
@@ -6880,32 +6813,48 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.128"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"
|
||||
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.78"
|
||||
version = "0.4.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928"
|
||||
checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.128"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"
|
||||
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -6913,22 +6862,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.128"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"
|
||||
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.4",
|
||||
"syn 2.0.118",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.128"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"
|
||||
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -6948,9 +6897,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.105"
|
||||
version = "0.3.77"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8"
|
||||
checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
@@ -7460,7 +7409,7 @@ dependencies = [
|
||||
"futures",
|
||||
"log",
|
||||
"serde",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.21",
|
||||
"windows 0.59.0",
|
||||
"windows-core 0.59.0",
|
||||
]
|
||||
@@ -7634,7 +7583,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"zerocopy-derive",
|
||||
"zerocopy-derive 0.7.35",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.59"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb"
|
||||
dependencies = [
|
||||
"zerocopy-derive 0.8.59",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7648,6 +7606,17 @@ dependencies = [
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.59"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.5"
|
||||
|
||||
+6
-7
@@ -36,7 +36,6 @@ strip = true
|
||||
|
||||
[dependencies]
|
||||
deltachat_derive = { path = "./deltachat_derive" }
|
||||
deltachat-time = { path = "./deltachat-time" }
|
||||
deltachat-contact-tools = { workspace = true }
|
||||
format-flowed = { path = "./format-flowed" }
|
||||
ratelimit = { path = "./deltachat-ratelimit" }
|
||||
@@ -85,9 +84,8 @@ quick-xml = { version = "0.41", features = ["escape-html"] }
|
||||
rand-old = { package = "rand", version = "0.8" }
|
||||
rand = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rusqlite = { workspace = true, features = ["backup"] }
|
||||
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||
sanitize-filename = { workspace = true }
|
||||
sdp = "0.17.1"
|
||||
serde_json = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
sha-1 = "0.10"
|
||||
@@ -102,7 +100,7 @@ thiserror = { workspace = true }
|
||||
tokio-io-timeout = "1.2.1"
|
||||
tokio-rustls = { version = "0.26.2", default-features = false, features = ["tls12", "brotli"] }
|
||||
tokio-stream = { version = "0.1.17", features = ["fs"] }
|
||||
astral-tokio-tar = { version = "0.6.3", default-features = false }
|
||||
astral-tokio-tar = { version = "0.7.0", default-features = false }
|
||||
tokio-util = { workspace = true }
|
||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
toml = "0.9"
|
||||
@@ -115,6 +113,7 @@ webpki-roots = "0.26.8"
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
criterion = { version = "0.8.1", features = ["async_tokio"] }
|
||||
deltachat-time = { path = "./deltachat-time" }
|
||||
futures-lite = { workspace = true }
|
||||
log = { workspace = true }
|
||||
nu-ansi-term = { workspace = true }
|
||||
@@ -189,12 +188,12 @@ futures = "0.3.32"
|
||||
futures-lite = "2.6.1"
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
mailparse = "0.16.1"
|
||||
mailparse = "0.17.0"
|
||||
nu-ansi-term = "0.50"
|
||||
num-traits = "0.2"
|
||||
rand = "0.9"
|
||||
regex = "1.12"
|
||||
rusqlite = "0.40.2"
|
||||
rusqlite = "0.37"
|
||||
sanitize-filename = "0.6"
|
||||
serde = "1.0"
|
||||
serde_json = "1"
|
||||
@@ -209,7 +208,7 @@ yerpc = "0.7"
|
||||
default = ["vendored"]
|
||||
internals = []
|
||||
vendored = [
|
||||
"rusqlite/bundled",
|
||||
"rusqlite/bundled-sqlcipher-vendored-openssl",
|
||||
"async-native-tls/vendored"
|
||||
]
|
||||
|
||||
|
||||
@@ -21,9 +21,9 @@ fn main() {
|
||||
url = env::var("CARGO_PKG_HOMEPAGE").unwrap_or_else(|_| "".to_string()),
|
||||
version = env::var("CARGO_PKG_VERSION").unwrap(),
|
||||
libs_priv = libs_priv,
|
||||
prefix = env::var("PREFIX").unwrap_or_else(|_| "/usr/local".to_string()),
|
||||
libdir = env::var("LIBDIR").unwrap_or_else(|_| "/usr/local/lib".to_string()),
|
||||
includedir = env::var("INCLUDEDIR").unwrap_or_else(|_| "/usr/local/include".to_string()),
|
||||
prefix = option_env!("PREFIX").unwrap_or_else(|| "/usr/local"),
|
||||
libdir = option_env!("LIBDIR").unwrap_or_else(|| "/usr/local/lib"),
|
||||
includedir = option_env!("INCLUDEDIR").unwrap_or_else(|| "/usr/local/include"),
|
||||
);
|
||||
|
||||
fs::create_dir_all(target_path.join("pkgconfig")).unwrap();
|
||||
|
||||
@@ -304,6 +304,21 @@ dc_context_t* dc_context_new_closed (const char* dbfile);
|
||||
int dc_context_open (dc_context_t *context, const char* passphrase);
|
||||
|
||||
|
||||
/**
|
||||
* Changes the passphrase on the open database.
|
||||
* Deprecated 2025-11, see `dc_context_open()` for reasoning.
|
||||
*
|
||||
* Existing database must already be encrypted and the passphrase cannot be NULL or empty.
|
||||
* It is impossible to encrypt unencrypted database with this method and vice versa.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param passphrase The new passphrase.
|
||||
* @return 1 on success, 0 on error.
|
||||
*/
|
||||
int dc_context_change_passphrase (dc_context_t* context, const char* passphrase);
|
||||
|
||||
|
||||
/**
|
||||
* Returns 1 if database is open.
|
||||
*
|
||||
|
||||
@@ -160,6 +160,24 @@ pub unsafe extern "C" fn dc_context_open(
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn dc_context_change_passphrase(
|
||||
context: *mut dc_context_t,
|
||||
passphrase: *const libc::c_char,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_context_change_passphrase()");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let ctx = unsafe { &*context };
|
||||
let passphrase = to_string_lossy(passphrase);
|
||||
block_on(ctx.change_passphrase(passphrase))
|
||||
.context("dc_context_change_passphrase() failed")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn dc_context_is_open(context: *mut dc_context_t) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
|
||||
@@ -12,7 +12,7 @@ pub struct JsonrpcReaction {
|
||||
emoji: String,
|
||||
|
||||
/// Emoji frequency.
|
||||
count: u32,
|
||||
count: usize,
|
||||
|
||||
/// True if we reacted with this emoji.
|
||||
is_from_self: bool,
|
||||
|
||||
@@ -8,7 +8,7 @@ repository = "https://github.com/chatmail/core"
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
deltachat = { workspace = true, features = ["internals"]}
|
||||
dirs = "6"
|
||||
dirs = "7"
|
||||
log = { workspace = true }
|
||||
nu-ansi-term = { workspace = true }
|
||||
qr2term = "0.3.3"
|
||||
|
||||
@@ -20,62 +20,13 @@ use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||
use deltachat::mimeparser::SystemMessage;
|
||||
use deltachat::peer_channels::{send_webxdc_realtime_advertisement, send_webxdc_realtime_data};
|
||||
use deltachat::qr::*;
|
||||
use deltachat::qr_code_generator::create_qr_svg;
|
||||
use deltachat::qr_code_generator::{create_qr_svg, get_securejoin_qr_svg};
|
||||
use deltachat::reaction::send_reaction;
|
||||
use deltachat::receive_imf::*;
|
||||
use deltachat::sql;
|
||||
use deltachat::tools::*;
|
||||
use tokio::fs;
|
||||
|
||||
/// Reset database tables.
|
||||
/// Argument is a bitmask, executing single or multiple actions in one call.
|
||||
/// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4.
|
||||
async fn reset_tables(context: &Context, bits: i32) {
|
||||
println!("Resetting tables ({bits})...");
|
||||
if 0 != bits & 4 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM keypairs;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
println!("(4) Private keypairs reset.");
|
||||
}
|
||||
if 0 != bits & 8 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM contacts WHERE id>9;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM chats WHERE id>9;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM chats_contacts;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM msgs WHERE id>9;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute(
|
||||
"DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';",
|
||||
(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
context.sql().config_cache().write().await.clear();
|
||||
println!("(8) Rest but server config reset.");
|
||||
}
|
||||
|
||||
context.emit_msgs_changed_without_ids();
|
||||
}
|
||||
|
||||
async fn poke_eml_file(context: &Context, filename: &Path) -> Result<()> {
|
||||
let data = read_file(context, filename).await?;
|
||||
|
||||
@@ -304,7 +255,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
export-keys\n\
|
||||
import-keys <key-file>\n\
|
||||
poke [<eml-file>|<folder>|<addr> <key-file>]\n\
|
||||
reset <flags>\n\
|
||||
stop\n\
|
||||
============================================="
|
||||
),
|
||||
@@ -396,9 +346,57 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
============================================="
|
||||
),
|
||||
},
|
||||
"connect" => {
|
||||
context.start_io().await;
|
||||
}
|
||||
"disconnect" => {
|
||||
context.stop_io().await;
|
||||
}
|
||||
"fetch" => {
|
||||
context.background_fetch().await?;
|
||||
}
|
||||
"configure" => {
|
||||
context.configure().await?;
|
||||
}
|
||||
"has-backup" => {
|
||||
has_backup(&context, blobdir).await?;
|
||||
}
|
||||
"clear" => {
|
||||
println!("\n\n\n");
|
||||
print!("\x1b[1;1H\x1b[2J");
|
||||
}
|
||||
"getqr" | "getbadqr" => {
|
||||
context.start_io().await;
|
||||
let group = arg1.parse::<u32>().ok().map(ChatId::new);
|
||||
let mut qr = deltachat::securejoin::get_securejoin_qr(&context, group).await?;
|
||||
if !qr.is_empty() {
|
||||
if arg0 == "getbadqr" && qr.len() > 40 {
|
||||
qr.replace_range(12..22, "0000000000")
|
||||
}
|
||||
println!("{qr}");
|
||||
qr2term::print_qr(qr.as_str())?;
|
||||
}
|
||||
}
|
||||
"getqrsvg" => {
|
||||
context.start_io().await;
|
||||
let group = arg1.parse::<u32>().ok().map(ChatId::new);
|
||||
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
|
||||
match get_securejoin_qr_svg(&context, group).await {
|
||||
Ok(svg) => {
|
||||
fs::write(&file, svg).await?;
|
||||
println!("QR code svg written to: {file:#?}");
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("Failed to get QR code svg: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
"joinqr" => {
|
||||
context.start_io().await;
|
||||
if !arg0.is_empty() {
|
||||
deltachat::securejoin::join_securejoin(&context, arg1).await?;
|
||||
}
|
||||
}
|
||||
"export-backup" => {
|
||||
let dir = dirs::home_dir().unwrap_or_default();
|
||||
imex(
|
||||
@@ -444,15 +442,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"poke" => {
|
||||
ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed");
|
||||
}
|
||||
"reset" => {
|
||||
ensure!(
|
||||
!arg1.is_empty(),
|
||||
"Argument <bits> missing: 4=private keys, 8=rest but server config"
|
||||
);
|
||||
let bits: i32 = arg1.parse()?;
|
||||
ensure!(bits < 16, "<bits> must be lower than 16.");
|
||||
reset_tables(&context, bits).await;
|
||||
}
|
||||
"stop" => {
|
||||
context.stop_ongoing().await;
|
||||
}
|
||||
|
||||
@@ -13,8 +13,6 @@ use anyhow::{Error, bail};
|
||||
use deltachat::EventType;
|
||||
use deltachat::chat::ChatId;
|
||||
use deltachat::context::*;
|
||||
use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
||||
use deltachat::securejoin::*;
|
||||
use log::{error, info, warn};
|
||||
use nu_ansi_term::Color;
|
||||
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
||||
@@ -25,7 +23,6 @@ use rustyline::validate::Validator;
|
||||
use rustyline::{
|
||||
Cmd, CompletionType, Config, Context as RustyContext, EditMode, Editor, Helper, KeyEvent,
|
||||
};
|
||||
use tokio::fs;
|
||||
use tokio::runtime::Handle;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
@@ -147,7 +144,7 @@ impl Completer for DcHelper {
|
||||
}
|
||||
}
|
||||
|
||||
const IMEX_COMMANDS: [&str; 10] = [
|
||||
const IMEX_COMMANDS: [&str; 9] = [
|
||||
"has-backup",
|
||||
"export-backup",
|
||||
"import-backup",
|
||||
@@ -156,7 +153,6 @@ const IMEX_COMMANDS: [&str; 10] = [
|
||||
"export-keys",
|
||||
"import-keys",
|
||||
"poke",
|
||||
"reset",
|
||||
"stop",
|
||||
];
|
||||
|
||||
@@ -353,23 +349,16 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
Ok(line) => {
|
||||
// TODO: ignore "set mail_pw"
|
||||
rl.add_history_entry(line.as_str())?;
|
||||
let should_continue = Handle::current().block_on(async {
|
||||
match handle_cmd(line.trim(), ctx.clone(), &mut selected_chat).await {
|
||||
Ok(ExitResult::Continue) => true,
|
||||
Ok(ExitResult::Exit) => {
|
||||
println!("Exiting ...");
|
||||
false
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Error: {err:#}");
|
||||
true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if !should_continue {
|
||||
let line = line.trim();
|
||||
if matches!(line, "exit" | "quit") {
|
||||
println!("Exiting ...");
|
||||
break;
|
||||
}
|
||||
if let Err(err) =
|
||||
Handle::current().block_on(cmdline(ctx.clone(), line, &mut selected_chat))
|
||||
{
|
||||
eprintln!("Error: {err:#}");
|
||||
}
|
||||
}
|
||||
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
|
||||
println!("Exiting...");
|
||||
@@ -393,77 +382,6 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ExitResult {
|
||||
Continue,
|
||||
Exit,
|
||||
}
|
||||
|
||||
async fn handle_cmd(
|
||||
line: &str,
|
||||
ctx: Context,
|
||||
selected_chat: &mut ChatId,
|
||||
) -> Result<ExitResult, Error> {
|
||||
let mut args = line.splitn(2, ' ');
|
||||
let arg0 = args.next().unwrap_or_default();
|
||||
let arg1 = args.next().unwrap_or_default();
|
||||
|
||||
match arg0 {
|
||||
"connect" => {
|
||||
ctx.start_io().await;
|
||||
}
|
||||
"disconnect" => {
|
||||
ctx.stop_io().await;
|
||||
}
|
||||
"fetch" => {
|
||||
ctx.background_fetch().await?;
|
||||
}
|
||||
"configure" => {
|
||||
ctx.configure().await?;
|
||||
}
|
||||
"clear" => {
|
||||
println!("\n\n\n");
|
||||
print!("\x1b[1;1H\x1b[2J");
|
||||
}
|
||||
"getqr" | "getbadqr" => {
|
||||
ctx.start_io().await;
|
||||
let group = arg1.parse::<u32>().ok().map(ChatId::new);
|
||||
let mut qr = get_securejoin_qr(&ctx, group).await?;
|
||||
if !qr.is_empty() {
|
||||
if arg0 == "getbadqr" && qr.len() > 40 {
|
||||
qr.replace_range(12..22, "0000000000")
|
||||
}
|
||||
println!("{qr}");
|
||||
qr2term::print_qr(qr.as_str())?;
|
||||
}
|
||||
}
|
||||
"getqrsvg" => {
|
||||
ctx.start_io().await;
|
||||
let group = arg1.parse::<u32>().ok().map(ChatId::new);
|
||||
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
|
||||
match get_securejoin_qr_svg(&ctx, group).await {
|
||||
Ok(svg) => {
|
||||
fs::write(&file, svg).await?;
|
||||
println!("QR code svg written to: {file:#?}");
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("Failed to get QR code svg: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
"joinqr" => {
|
||||
ctx.start_io().await;
|
||||
if !arg0.is_empty() {
|
||||
join_securejoin(&ctx, arg1).await?;
|
||||
}
|
||||
}
|
||||
"exit" | "quit" => return Ok(ExitResult::Exit),
|
||||
_ => cmdline(ctx.clone(), line, selected_chat).await?,
|
||||
}
|
||||
|
||||
Ok(ExitResult::Continue)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Error> {
|
||||
tracing_subscriber::fmt()
|
||||
|
||||
@@ -149,7 +149,10 @@ class Rpc:
|
||||
def close(self) -> None:
|
||||
"""Terminate RPC server process and wait until the reader loop finishes."""
|
||||
self.closing = True
|
||||
self.stop_io_for_all_accounts()
|
||||
# JSON-RPC error may happen if RPC server process has crashed already.
|
||||
# We still want to shutdown all threads in this case.
|
||||
with contextlib.suppress(JsonRpcError):
|
||||
self.stop_io_for_all_accounts()
|
||||
# Let `events_loop` stop cleanly on `closing` before the pipe goes away,
|
||||
# otherwise it might exit through an "RPC server closed" error instead.
|
||||
self.events_thread.join()
|
||||
|
||||
@@ -351,12 +351,11 @@ def test_qr_works_after_removing_primary_transport(acf, log) -> None:
|
||||
log.section("Alice creates a QR code")
|
||||
chat_qr = alice.get_qr_code()
|
||||
chat_qr_unquoted = urllib.parse.unquote(chat_qr)
|
||||
assert f"&a={first_addr}" in chat_qr_unquoted
|
||||
assert f"&r={third_addr},{second_addr}" in chat_qr_unquoted
|
||||
assert f"&a={third_addr}" in chat_qr_unquoted
|
||||
assert f"&r={second_addr},{first_addr}" in chat_qr_unquoted
|
||||
|
||||
log.section("Alice removes first and second transport")
|
||||
alice.set_config("configured_addr", third_addr)
|
||||
alice.delete_transport(first_addr)
|
||||
log.section("Alice removes the transport named by the a= parameter")
|
||||
alice.delete_transport(third_addr)
|
||||
alice.delete_transport(second_addr)
|
||||
|
||||
log.section("Bob scans the QR code, which still works")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -90,6 +91,30 @@ def test_qr_securejoin(acf):
|
||||
fiona.wait_for_securejoin_joiner_success()
|
||||
|
||||
|
||||
def test_qr_securejoin_request_deleted_on_all_transports(acf):
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
alice.add_transport_from_qr(acf.get_account_qr())
|
||||
alice.bring_online()
|
||||
alice_chat = alice.create_group("Group")
|
||||
qr_code = alice_chat.get_qr_code()
|
||||
alice2 = alice.clone()
|
||||
|
||||
bob.secure_join(qr_code)
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
alice_chat.remove_contact(bob)
|
||||
# Stop and start io in order to wait until the remove message is sent out.
|
||||
alice.stop_io()
|
||||
alice.bring_online()
|
||||
|
||||
# Membership timestamps have a resolution of one second.
|
||||
time.sleep(1)
|
||||
# By now, the securejoin message sent by Bob must be deleted on Alice's relays.
|
||||
# Otherwise, alice2 would execute securejoin again and re-add Bob.
|
||||
alice2.bring_online()
|
||||
alice2_chat = alice2.get_chat_by_id(alice_chat.id)
|
||||
assert alice2.create_contact(bob) not in alice2_chat.get_contacts()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("all_devices_online", [True, False])
|
||||
def test_qr_securejoin_broadcast(acf, all_devices_online):
|
||||
alice, bob, fiona = acf.get_online_accounts(3)
|
||||
|
||||
@@ -35,6 +35,19 @@ def test_sleep(rpc) -> None:
|
||||
assert sleep_5_future in pending
|
||||
|
||||
|
||||
def test_shutdown(rpc) -> None:
|
||||
"""Test RPC client shutdown if RPC server process is terminated.
|
||||
|
||||
This is a regression test, at the time of adding it RPC client
|
||||
did not terminate all threads and python process did not exit.
|
||||
Shutting down all threads is tested implicitly by pytest-timeout.
|
||||
"""
|
||||
rpc.process.kill()
|
||||
rpc.process.wait()
|
||||
with pytest.raises(JsonRpcError):
|
||||
rpc.get_system_info()
|
||||
|
||||
|
||||
def test_email_address_validity(rpc) -> None:
|
||||
valid_addresses = [
|
||||
"email@example.com",
|
||||
|
||||
@@ -69,11 +69,8 @@ skip = [
|
||||
{ name = "derive_more-impl", version = "1.0.0" },
|
||||
{ name = "derive_more", version = "1.0.0" },
|
||||
{ name = "event-listener", version = "2.5.3" },
|
||||
{ name = "foldhash", version = "0.1.5" },
|
||||
{ name = "getrandom", version = "0.2.12" },
|
||||
{ name = "getrandom", version = "0.3.3" },
|
||||
{ name = "hashbrown", version = "0.15.4" },
|
||||
{ name = "hashbrown", version = "0.16.1" },
|
||||
{ name = "heck", version = "0.4.1" },
|
||||
{ name = "http", version = "0.2.12" },
|
||||
{ name = "hybrid-array", version = "0.2.3" },
|
||||
@@ -116,6 +113,8 @@ skip = [
|
||||
{ name = "windows_x86_64_gnu" },
|
||||
{ name = "windows_x86_64_gnullvm" },
|
||||
{ name = "windows_x86_64_msvc" },
|
||||
{ name = "zerocopy-derive", version = "0.7.35" },
|
||||
{ name = "zerocopy", version = "0.7.35" },
|
||||
]
|
||||
|
||||
|
||||
|
||||
+52
-2
@@ -169,7 +169,9 @@ impl Accounts {
|
||||
.with_push_subscriber(self.push_subscriber.clone())
|
||||
.build()
|
||||
.await?;
|
||||
ctx.open().await?;
|
||||
// Try to open without a passphrase,
|
||||
// but do not return an error if account is passphare-protected.
|
||||
ctx.open("".to_string()).await?;
|
||||
|
||||
self.accounts.insert(account_config.id, ctx);
|
||||
self.emit_event(EventType::AccountsChanged);
|
||||
@@ -819,7 +821,9 @@ impl Config {
|
||||
.build()
|
||||
.await
|
||||
.with_context(|| format!("failed to create context from file {dbfile:?}"))?;
|
||||
ctx.open().await?;
|
||||
// Try to open without a passphrase,
|
||||
// but do not return an error if account is passphare-protected.
|
||||
ctx.open("".to_string()).await?;
|
||||
|
||||
accounts.insert(account_config.id, ctx);
|
||||
}
|
||||
@@ -1268,6 +1272,52 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_encrypted_account() -> Result<()> {
|
||||
let dir = tempfile::tempdir().context("failed to create tempdir")?;
|
||||
let p: PathBuf = dir.path().join("accounts");
|
||||
|
||||
let writable = true;
|
||||
let mut accounts = Accounts::new(p.clone(), writable)
|
||||
.await
|
||||
.context("failed to create accounts manager")?;
|
||||
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
let account_id = accounts
|
||||
.add_closed_account()
|
||||
.await
|
||||
.context("failed to add closed account")?;
|
||||
let account = accounts
|
||||
.get_selected_account()
|
||||
.context("failed to get account")?;
|
||||
assert_eq!(account.id, account_id);
|
||||
let passphrase_set_success = account
|
||||
.open("foobar".to_string())
|
||||
.await
|
||||
.context("failed to set passphrase")?;
|
||||
assert!(passphrase_set_success);
|
||||
drop(accounts);
|
||||
|
||||
let writable = false;
|
||||
let accounts = Accounts::new(p.clone(), writable)
|
||||
.await
|
||||
.context("failed to create second accounts manager")?;
|
||||
let account = accounts
|
||||
.get_selected_account()
|
||||
.context("failed to get account")?;
|
||||
assert_eq!(account.is_open().await, false);
|
||||
|
||||
// Try wrong passphrase.
|
||||
assert_eq!(account.open("barfoo".to_string()).await?, false);
|
||||
assert_eq!(account.open("".to_string()).await?, false);
|
||||
|
||||
assert_eq!(account.open("foobar".to_string()).await?, true);
|
||||
assert_eq!(account.is_open().await, true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that accounts share stock string translations.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_accounts_share_translations() -> Result<()> {
|
||||
|
||||
+3
-1
@@ -34,6 +34,8 @@ const DEFAULT_RELAY_CANDIDATES: &[&str] = &[
|
||||
"chat.nuvon.app",
|
||||
"chat.tinydispatch.org",
|
||||
"chat.vim.wtf",
|
||||
"chatmail.au",
|
||||
"chatmail.cc",
|
||||
"chatmail.uk",
|
||||
"chtml.ca",
|
||||
"deltachat.me",
|
||||
@@ -163,7 +165,7 @@ async fn maybe_add_additional_relays_inner(context: &Context, skip_network: bool
|
||||
for _ in 0..NUM_TRANSPORTS_TARGET {
|
||||
if context.count_transports().await? >= NUM_TRANSPORTS_TARGET {
|
||||
context
|
||||
.set_config_internal(Config::AutorelayFinished, config::from_bool(true))
|
||||
.set_config_internal(Config::AutorelayFinished, Some(config::from_bool(true)))
|
||||
.await?;
|
||||
|
||||
return Ok(relay_added);
|
||||
|
||||
+8
-30
@@ -164,9 +164,9 @@ impl<'a> BlobObject<'a> {
|
||||
/// you want to create a [BlobObject] for a filename read from the
|
||||
/// database.
|
||||
pub fn from_name(context: &'a Context, name: &str) -> Result<BlobObject<'a>> {
|
||||
let name = match name.starts_with("$BLOBDIR/") {
|
||||
true => name.splitn(2, '/').last().unwrap(),
|
||||
false => name,
|
||||
let name = match name.strip_prefix("$BLOBDIR/") {
|
||||
Some(name) => name,
|
||||
None => name,
|
||||
};
|
||||
if !BlobObject::is_acceptible_blob_name(name) {
|
||||
return Err(format_err!("not an acceptable blob name: {name}"));
|
||||
@@ -289,6 +289,7 @@ impl<'a> BlobObject<'a> {
|
||||
name: Option<String>,
|
||||
viewtype: &mut Viewtype,
|
||||
) -> Result<String> {
|
||||
debug_assert!(matches!(viewtype, Viewtype::File | Viewtype::Image));
|
||||
let (max_wh, max_bytes) =
|
||||
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
|
||||
.unwrap_or_default()
|
||||
@@ -326,6 +327,7 @@ impl<'a> BlobObject<'a> {
|
||||
max_bytes: usize,
|
||||
is_avatar: bool,
|
||||
) -> Result<String> {
|
||||
debug_assert!(matches!(viewtype, Viewtype::File | Viewtype::Image));
|
||||
// Add white background only to avatars to spare the CPU.
|
||||
let mut add_white_bg = is_avatar;
|
||||
let mut no_exif = false;
|
||||
@@ -340,17 +342,9 @@ impl<'a> BlobObject<'a> {
|
||||
// It's strange that BufReader modifies a file position while it takes a non-mut
|
||||
// reference. Ok, just rewind it.
|
||||
file.rewind()?;
|
||||
let imgreader = ImageReader::new(std::io::BufReader::new(&file)).with_guessed_format();
|
||||
let imgreader = match imgreader {
|
||||
Ok(ir) => ir,
|
||||
_ => {
|
||||
file.rewind()?;
|
||||
ImageReader::with_format(
|
||||
std::io::BufReader::new(&file),
|
||||
ImageFormat::from_path(self.to_abs_path())?,
|
||||
)
|
||||
}
|
||||
};
|
||||
let imgreader = ImageReader::new(std::io::BufReader::new(&file))
|
||||
.with_guessed_format()
|
||||
.context("Failed to guess image format due to I/O error")?;
|
||||
let fmt = imgreader.format().context("Unknown format")?;
|
||||
if *vt == Viewtype::File {
|
||||
*vt = Viewtype::Image;
|
||||
@@ -363,22 +357,6 @@ impl<'a> BlobObject<'a> {
|
||||
.unwrap_or(Orientation::NoTransforms);
|
||||
let mut encoded = Vec::new();
|
||||
|
||||
if *vt == Viewtype::Sticker {
|
||||
let x_max = img.width().saturating_sub(1);
|
||||
let y_max = img.height().saturating_sub(1);
|
||||
if !img.in_bounds(x_max, y_max)
|
||||
|| !(img.get_pixel(0, 0).0[3] == 0
|
||||
|| img.get_pixel(x_max, 0).0[3] == 0
|
||||
|| img.get_pixel(0, y_max).0[3] == 0
|
||||
|| img.get_pixel(x_max, y_max).0[3] == 0)
|
||||
{
|
||||
*vt = Viewtype::Image;
|
||||
} else {
|
||||
// Core doesn't auto-assign `Viewtype::Sticker` to messages and stickers coming
|
||||
// from UIs shouldn't contain sensitive Exif info.
|
||||
return Ok(name);
|
||||
}
|
||||
}
|
||||
img.apply_orientation(orientation);
|
||||
|
||||
// max_wh is the maximum image width and height, i.e. the resolution-limit,
|
||||
|
||||
+15
-10
@@ -2924,9 +2924,7 @@ async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Ve
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(ref side_effects) = side_effects {
|
||||
msg.subject.clone_from(&side_effects.subject);
|
||||
}
|
||||
msg.subject.clone_from(&side_effects.subject);
|
||||
if is_encrypted {
|
||||
msg.param.set_int(Param::GuaranteeE2ee, 1);
|
||||
} else {
|
||||
@@ -2968,13 +2966,13 @@ async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Ve
|
||||
now,
|
||||
msg.id,
|
||||
&queued_pre_msg,
|
||||
pre_side_effects.as_ref(),
|
||||
Some(&pre_side_effects),
|
||||
)
|
||||
.context("Failed to enqueue pre-message")?;
|
||||
row_ids.push(row_id)
|
||||
}
|
||||
row_ids.push(
|
||||
enqueue_mail(transaction, now, msg.id, &queued_msg, side_effects.as_ref())
|
||||
enqueue_mail(transaction, now, msg.id, &queued_msg, Some(&side_effects))
|
||||
.context("Failed to enqueue message")?,
|
||||
);
|
||||
Ok(row_ids)
|
||||
@@ -3639,7 +3637,7 @@ pub(crate) async fn create_out_broadcast_ext(
|
||||
)?;
|
||||
ensure!(cnt == 0, "{cnt} chats exist with grpid {grpid}");
|
||||
let mut params: Params = Params::new();
|
||||
params.update_timestamp(Param::GroupNameTimestamp, time())?;
|
||||
params.update_timestamp(Param::GroupNameTimestamp, time());
|
||||
|
||||
t.execute(
|
||||
"INSERT INTO chats
|
||||
@@ -3985,7 +3983,7 @@ ORDER BY timestamp DESC, id DESC -- final ORDER BY is needed as UNION does not g
|
||||
(
|
||||
chat_id,
|
||||
Viewtype::Webxdc,
|
||||
constants::N_MSGS_TO_NEW_BROADCAST_MEMBER as u32,
|
||||
constants::N_MSGS_TO_NEW_BROADCAST_MEMBER,
|
||||
ContactId::INFO,
|
||||
),
|
||||
|row: &rusqlite::Row| Ok(row.get::<_, MsgId>(0)?),
|
||||
@@ -4795,12 +4793,14 @@ pub(crate) async fn get_chat_id_by_grpid(
|
||||
///
|
||||
/// Optional `label` can be provided to ensure that message is added only once.
|
||||
/// If `important` is true, a notification will be sent.
|
||||
/// `timestamp_sent` is the time shown on the message; it does not affect ordering.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub async fn add_device_msg_with_importance(
|
||||
context: &Context,
|
||||
label: Option<&str>,
|
||||
msg: Option<&mut Message>,
|
||||
important: bool,
|
||||
timestamp_sent: i64,
|
||||
) -> Result<MsgId> {
|
||||
ensure!(
|
||||
label.is_some() || msg.is_some(),
|
||||
@@ -4820,11 +4820,10 @@ pub async fn add_device_msg_with_importance(
|
||||
chat_id = ChatId::get_for_contact(context, ContactId::DEVICE).await?;
|
||||
|
||||
let rfc724_mid = create_outgoing_rfc724_mid();
|
||||
let timestamp_sent = time();
|
||||
|
||||
// makes sure, the added message is the last one,
|
||||
// even if the date is wrong (useful esp. when warning about bad dates)
|
||||
msg.timestamp_sort = timestamp_sent;
|
||||
msg.timestamp_sort = time();
|
||||
if let Some(last_msg_time) = chat_id.get_timestamp(context).await?
|
||||
&& msg.timestamp_sort <= last_msg_time
|
||||
{
|
||||
@@ -4892,7 +4891,7 @@ pub async fn add_device_msg(
|
||||
label: Option<&str>,
|
||||
msg: Option<&mut Message>,
|
||||
) -> Result<MsgId> {
|
||||
add_device_msg_with_importance(context, label, msg, false).await
|
||||
add_device_msg_with_importance(context, label, msg, false, time()).await
|
||||
}
|
||||
|
||||
/// Returns true if device message with a given label was ever added to the device chat.
|
||||
@@ -4920,6 +4919,12 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
|
||||
.execute("DELETE FROM msgs WHERE from_id=?;", (ContactId::DEVICE,))
|
||||
.await?;
|
||||
context.sql.execute("DELETE FROM devmsglabels;", ()).await?;
|
||||
context
|
||||
.set_config_internal(Config::BackupTransferMsgId, None)
|
||||
.await?;
|
||||
context
|
||||
.set_config_internal(Config::BackupTransferTimestamp, None)
|
||||
.await?;
|
||||
|
||||
// Insert labels for welcome messages to avoid them being re-added on reconfiguration.
|
||||
context
|
||||
|
||||
+14
-3
@@ -336,6 +336,17 @@ pub enum Config {
|
||||
/// Timestamp of the last time housekeeping was run
|
||||
LastHousekeeping,
|
||||
|
||||
/// ID of the device message added after backup transfer.
|
||||
///
|
||||
/// If the message does not exist on the first housekeeping
|
||||
/// after `READD_BACKUP_TRANSFER_MSG_DELAY`, it is re-added.
|
||||
BackupTransferMsgId,
|
||||
|
||||
/// Timestamp of the device message added after backup transfer.
|
||||
///
|
||||
/// Needed to check against `READD_BACKUP_TRANSFER_MSG_DELAY`.
|
||||
BackupTransferTimestamp,
|
||||
|
||||
/// Timestamp of the last time accumulated broadcast channel reactions were sent
|
||||
LastReactionsBroadcast,
|
||||
|
||||
@@ -809,7 +820,7 @@ impl Context {
|
||||
|
||||
/// Set the given config to a boolean value.
|
||||
pub async fn set_config_bool(&self, key: Config, value: bool) -> Result<()> {
|
||||
self.set_config(key, from_bool(value)).await?;
|
||||
self.set_config(key, Some(from_bool(value))).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -830,8 +841,8 @@ impl Context {
|
||||
}
|
||||
|
||||
/// Returns a value for use in `Context::set_config_*()` for the given `bool`.
|
||||
pub(crate) fn from_bool(val: bool) -> Option<&'static str> {
|
||||
Some(if val { "1" } else { "0" })
|
||||
pub(crate) fn from_bool(val: bool) -> &'static str {
|
||||
if val { "1" } else { "0" }
|
||||
}
|
||||
|
||||
pub(crate) fn bool_from_config(config: Option<&str>) -> bool {
|
||||
|
||||
@@ -29,7 +29,7 @@ impl ServerParams {
|
||||
if self.username.is_empty() {
|
||||
vec![Self {
|
||||
username: addr.to_string(),
|
||||
..self.clone()
|
||||
..self
|
||||
}]
|
||||
} else {
|
||||
vec![self]
|
||||
|
||||
+52
-6
@@ -67,6 +67,7 @@ pub struct ContextBuilder {
|
||||
id: u32,
|
||||
events: Events,
|
||||
stock_strings: StockStrings,
|
||||
password: Option<String>,
|
||||
|
||||
push_subscriber: Option<PushSubscriber>,
|
||||
}
|
||||
@@ -83,6 +84,7 @@ impl ContextBuilder {
|
||||
id: rand::random(),
|
||||
events: Events::new(),
|
||||
stock_strings: StockStrings::new(),
|
||||
password: None,
|
||||
push_subscriber: None,
|
||||
}
|
||||
}
|
||||
@@ -129,6 +131,19 @@ impl ContextBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the password to unlock the database.
|
||||
/// Deprecated 2025-11:
|
||||
/// - Db encryption does nothing with blobs, so fs/disk encryption is recommended.
|
||||
/// - Isolation from other apps is needed anyway.
|
||||
///
|
||||
/// If an encrypted database is used it must be opened with a password. Setting a
|
||||
/// password on a new database will enable encryption.
|
||||
#[deprecated(since = "TBD")]
|
||||
pub fn with_password(mut self, password: String) -> Self {
|
||||
self.password = Some(password);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets push subscriber.
|
||||
pub(crate) fn with_push_subscriber(mut self, push_subscriber: PushSubscriber) -> Self {
|
||||
self.push_subscriber = Some(push_subscriber);
|
||||
@@ -153,10 +168,11 @@ impl ContextBuilder {
|
||||
///
|
||||
/// Returns error if context cannot be opened.
|
||||
pub async fn open(self) -> Result<Context> {
|
||||
let password = self.password.clone().unwrap_or_default();
|
||||
let context = self.build().await?;
|
||||
match context.open().await? {
|
||||
match context.open(password).await? {
|
||||
true => Ok(context),
|
||||
false => bail!("FIXME database could not be decrypted, incorrect or missing password"),
|
||||
false => bail!("database could not be decrypted, incorrect or missing password"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -370,7 +386,10 @@ impl Context {
|
||||
let context =
|
||||
Self::new_closed(dbfile, id, events, stock_strings, Default::default()).await?;
|
||||
|
||||
context.sql.open(&context).await?;
|
||||
// Open the database if is not encrypted.
|
||||
if context.check_passphrase("".to_string()).await? {
|
||||
context.sql.open(&context, "".to_string()).await?;
|
||||
}
|
||||
Ok(context)
|
||||
}
|
||||
|
||||
@@ -414,9 +433,20 @@ impl Context {
|
||||
/// Returns true if passphrase is correct, false is passphrase is not correct. Fails on other
|
||||
/// errors.
|
||||
#[deprecated(since = "TBD")]
|
||||
pub async fn open(&self) -> Result<bool> {
|
||||
self.sql.open(self).await?;
|
||||
Ok(true)
|
||||
pub async fn open(&self, passphrase: String) -> Result<bool> {
|
||||
if self.sql.check_passphrase(passphrase.clone()).await? {
|
||||
self.sql.open(self, passphrase).await?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes encrypted database passphrase.
|
||||
/// Deprecated 2025-11, see [`ContextBuilder::with_password()`] for reasoning.
|
||||
pub async fn change_passphrase(&self, passphrase: String) -> Result<()> {
|
||||
self.sql.change_passphrase(passphrase).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns true if database is open.
|
||||
@@ -424,6 +454,15 @@ impl Context {
|
||||
self.sql.is_open().await
|
||||
}
|
||||
|
||||
/// Tests the database passphrase.
|
||||
///
|
||||
/// Returns true if passphrase is correct.
|
||||
///
|
||||
/// Fails if database is already open.
|
||||
pub(crate) async fn check_passphrase(&self, passphrase: String) -> Result<bool> {
|
||||
self.sql.check_passphrase(passphrase).await
|
||||
}
|
||||
|
||||
pub(crate) fn with_blobdir(
|
||||
dbfile: PathBuf,
|
||||
blobdir: PathBuf,
|
||||
@@ -811,6 +850,13 @@ impl Context {
|
||||
res.insert("number_of_contacts", contacts.to_string());
|
||||
res.insert("database_dir", self.get_dbfile().display().to_string());
|
||||
res.insert("database_version", dbversion.to_string());
|
||||
res.insert(
|
||||
"database_encrypted",
|
||||
self.sql
|
||||
.is_encrypted()
|
||||
.await
|
||||
.map_or_else(|| "closed".to_string(), |b| b.to_string()),
|
||||
);
|
||||
res.insert("journal_mode", journal_mode);
|
||||
res.insert("blobdir", self.get_blobdir().display().to_string());
|
||||
res.insert(
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use anyhow::Context as _;
|
||||
use strum::IntoEnumIterator;
|
||||
use tempfile::tempdir;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{Chat, MuteDuration, get_chat_contacts, get_chat_msgs, send_msg, set_muted};
|
||||
@@ -292,6 +294,8 @@ async fn test_get_info_completeness() {
|
||||
// too sensitive or summarized in another item.
|
||||
let skip_from_get_info = vec![
|
||||
"addr",
|
||||
"backup_transfer_msg_id",
|
||||
"backup_transfer_timestamp",
|
||||
"displayname",
|
||||
"imap_certificate_checks",
|
||||
"mail_server",
|
||||
@@ -484,6 +488,63 @@ async fn test_limit_search_msgs() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_check_passphrase() -> Result<()> {
|
||||
let dir = tempdir()?;
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
|
||||
let context = ContextBuilder::new(dbfile.clone())
|
||||
.with_id(1)
|
||||
.build()
|
||||
.await
|
||||
.context("failed to create context")?;
|
||||
assert_eq!(context.open("foo".to_string()).await?, true);
|
||||
assert_eq!(context.is_open().await, true);
|
||||
drop(context);
|
||||
|
||||
let context = ContextBuilder::new(dbfile)
|
||||
.with_id(2)
|
||||
.build()
|
||||
.await
|
||||
.context("failed to create context")?;
|
||||
assert_eq!(context.is_open().await, false);
|
||||
assert_eq!(context.check_passphrase("bar".to_string()).await?, false);
|
||||
assert_eq!(context.open("false".to_string()).await?, false);
|
||||
assert_eq!(context.open("foo".to_string()).await?, true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_context_change_passphrase() -> Result<()> {
|
||||
let dir = tempdir()?;
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
|
||||
let context = ContextBuilder::new(dbfile)
|
||||
.with_id(1)
|
||||
.build()
|
||||
.await
|
||||
.context("failed to create context")?;
|
||||
assert_eq!(context.open("foo".to_string()).await?, true);
|
||||
assert_eq!(context.is_open().await, true);
|
||||
|
||||
context
|
||||
.set_config(Config::Addr, Some("alice@example.org"))
|
||||
.await?;
|
||||
|
||||
context
|
||||
.change_passphrase("bar".to_string())
|
||||
.await
|
||||
.context("Failed to change passphrase")?;
|
||||
|
||||
assert_eq!(
|
||||
context.get_config(Config::Addr).await?.unwrap(),
|
||||
"alice@example.org"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ongoing() -> Result<()> {
|
||||
let context = TestContext::new().await;
|
||||
|
||||
+2
-2
@@ -1454,7 +1454,7 @@ impl Session {
|
||||
/// or flags have been changed.
|
||||
/// In this case we may want to skip next IDLE and do a round
|
||||
/// of fetching new messages and synchronizing seen flags.
|
||||
fn drain_unsolicited_responses(&self, context: &Context) -> Result<bool> {
|
||||
fn drain_unsolicited_responses(&self, context: &Context) -> bool {
|
||||
use UnsolicitedResponse::*;
|
||||
use async_imap::imap_proto::Response;
|
||||
use async_imap::imap_proto::ResponseCode;
|
||||
@@ -1499,7 +1499,7 @@ impl Session {
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(should_refetch)
|
||||
should_refetch
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ impl Session {
|
||||
|
||||
self.select_with_uidvalidity(context, folder).await?;
|
||||
|
||||
if self.drain_unsolicited_responses(context)? {
|
||||
if self.drain_unsolicited_responses(context) {
|
||||
self.new_mail = true;
|
||||
}
|
||||
|
||||
|
||||
+25
-16
@@ -30,6 +30,7 @@ use crate::tools::{
|
||||
mod transfer;
|
||||
|
||||
use ::pgp::types::KeyDetails;
|
||||
pub(crate) use transfer::maybe_readd_backup_transfer_msg;
|
||||
pub use transfer::{BackupProvider, get_backup};
|
||||
|
||||
// Name of the database file in the backup.
|
||||
@@ -200,9 +201,6 @@ async fn import_backup(
|
||||
backup_to_import: &Path,
|
||||
passphrase: String,
|
||||
) -> Result<()> {
|
||||
if !passphrase.is_empty() {
|
||||
bail!("Encrypted passphrase is not supported");
|
||||
}
|
||||
let backup_file = File::open(backup_to_import).await?;
|
||||
let file_size = backup_file.metadata().await?.len();
|
||||
info!(
|
||||
@@ -213,7 +211,7 @@ async fn import_backup(
|
||||
context.get_dbfile().display()
|
||||
);
|
||||
|
||||
import_backup_stream(context, backup_file, file_size).await?;
|
||||
import_backup_stream(context, backup_file, file_size, passphrase).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -234,6 +232,7 @@ pub(crate) async fn import_backup_stream<R: tokio::io::AsyncRead + Unpin>(
|
||||
context: &Context,
|
||||
backup_file: R,
|
||||
file_size: u64,
|
||||
passphrase: String,
|
||||
) -> Result<()> {
|
||||
ensure!(
|
||||
!context.is_configured().await?,
|
||||
@@ -244,7 +243,7 @@ pub(crate) async fn import_backup_stream<R: tokio::io::AsyncRead + Unpin>(
|
||||
"Cannot import backup, IO is running"
|
||||
);
|
||||
|
||||
import_backup_stream_inner(context, backup_file, file_size)
|
||||
import_backup_stream_inner(context, backup_file, file_size, passphrase)
|
||||
.await
|
||||
.0
|
||||
}
|
||||
@@ -317,6 +316,7 @@ async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
|
||||
context: &Context,
|
||||
backup_file: R,
|
||||
file_size: u64,
|
||||
passphrase: String,
|
||||
) -> (Result<()>,) {
|
||||
let backup_file = ProgressReader::new(backup_file, context.clone(), file_size);
|
||||
let mut archive = Archive::new(backup_file);
|
||||
@@ -363,7 +363,7 @@ async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
|
||||
if res.is_ok() {
|
||||
res = context
|
||||
.sql
|
||||
.import(&unpacked_database)
|
||||
.import(&unpacked_database, passphrase.clone())
|
||||
.await
|
||||
.context("cannot import unpacked database");
|
||||
}
|
||||
@@ -391,7 +391,7 @@ async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.open(context)
|
||||
.open(context, "".to_string())
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
@@ -736,7 +736,7 @@ where
|
||||
/// overwritten.
|
||||
///
|
||||
/// This also verifies that IO is not running during the export.
|
||||
async fn export_database(context: &Context, dest: &Path, _passphrase: String) -> Result<()> {
|
||||
async fn export_database(context: &Context, dest: &Path, passphrase: String) -> Result<()> {
|
||||
ensure!(
|
||||
!context.scheduler.is_running().await,
|
||||
"cannot export backup, IO is running"
|
||||
@@ -746,7 +746,6 @@ async fn export_database(context: &Context, dest: &Path, _passphrase: String) ->
|
||||
let dest = dest
|
||||
.to_str()
|
||||
.with_context(|| format!("path {} is not valid unicode", dest.display()))?;
|
||||
let mut dest_conn = rusqlite::Connection::open(dest)?;
|
||||
|
||||
context.set_config(Config::BccSelf, Some("1")).await?;
|
||||
context
|
||||
@@ -757,12 +756,22 @@ async fn export_database(context: &Context, dest: &Path, _passphrase: String) ->
|
||||
context
|
||||
.sql
|
||||
.call_write(|conn| {
|
||||
if let Err(err) = conn.execute("VACUUM", ()) {
|
||||
warn!(context, "Vacuum failed, exporting anyway: {err:#}.");
|
||||
}
|
||||
|
||||
let backup = rusqlite::backup::Backup::new(conn, &mut dest_conn)?;
|
||||
backup.run_to_completion(5, std::time::Duration::ZERO, None)?;
|
||||
conn.execute("VACUUM;", ())
|
||||
.map_err(|err| warn!(context, "Vacuum failed, exporting anyway {err}"))
|
||||
.ok();
|
||||
conn.execute("ATTACH DATABASE ? AS backup KEY ?", (dest, passphrase))
|
||||
.context("failed to attach backup database")?;
|
||||
let res = conn
|
||||
.query_row("SELECT sqlcipher_export('backup')", [], |_row| Ok(()))
|
||||
.context("failed to export to attached backup database");
|
||||
conn.execute(
|
||||
"UPDATE backup.config SET value='0' WHERE keyname='verified_one_on_one_chats';",
|
||||
[],
|
||||
)
|
||||
.ok(); // Deprecated 2025-07. If verified_one_on_one_chats was not set, this errors, which we ignore
|
||||
conn.execute("DETACH DATABASE backup", [])
|
||||
.context("failed to detach backup database")?;
|
||||
res?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
@@ -1032,7 +1041,7 @@ mod tests {
|
||||
ar.unpack(&unpack_dir).await?;
|
||||
|
||||
let sql = sql::Sql::new(unpack_dir.path().join(DBFILE_BACKUP_NAME));
|
||||
sql.open(&context2).await?;
|
||||
sql.open(&context2, "".to_string()).await?;
|
||||
assert_eq!(
|
||||
sql.get_raw_config_int("backup_version").await?.unwrap(),
|
||||
DCBACKUP_VERSION
|
||||
|
||||
+99
-8
@@ -40,21 +40,68 @@ use tokio::task::JoinHandle;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::EventType;
|
||||
use crate::chat::add_device_msg;
|
||||
use crate::chat::add_device_msg_with_importance;
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::imex::BlobDirContents;
|
||||
use crate::key;
|
||||
use crate::log::warn;
|
||||
use crate::message::Message;
|
||||
use crate::message::{Message, MsgId};
|
||||
use crate::qr::Qr;
|
||||
use crate::stock_str::backup_transfer_msg_body;
|
||||
use crate::tools::{TempPathGuard, create_id};
|
||||
use crate::tools::{TempPathGuard, create_id, time};
|
||||
|
||||
use super::{DBFILE_BACKUP_NAME, export_backup_stream, export_database, import_backup_stream};
|
||||
|
||||
/// ALPN protocol identifier for the backup transfer protocol.
|
||||
const BACKUP_ALPN: &[u8] = b"/deltachat/backup";
|
||||
|
||||
/// Minimum time after a backup transfer before we check
|
||||
/// whether the "second device added" device message still exists.
|
||||
///
|
||||
/// The check is done in the first housekeeping after this delay;
|
||||
/// if the message was deleted until then, it is re-added with the original timestamp.
|
||||
/// After the check, the user can delete the message as usual without it being re-added.
|
||||
///
|
||||
/// This makes it easier to spot unwanted "add second device" actions.
|
||||
/// It is clear that this does not catch all eventualities, it is best-effort.
|
||||
/// First line of defense is device locking and asking for secret explicitly before adding a second device.
|
||||
const READD_BACKUP_TRANSFER_MSG_DELAY: i64 = 60 * 60;
|
||||
|
||||
/// Checks `Config::BackupTransferMsgId`
|
||||
/// and re-adds the backup transfer device message if it does not exist and some time passed.
|
||||
pub(crate) async fn maybe_readd_backup_transfer_msg(context: &Context) -> Result<()> {
|
||||
let Some(msg_id) = context
|
||||
.get_config_parsed::<u32>(Config::BackupTransferMsgId)
|
||||
.await?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let timestamp = context
|
||||
.get_config_i64(Config::BackupTransferTimestamp)
|
||||
.await?;
|
||||
if time() < timestamp.saturating_add(READD_BACKUP_TRANSFER_MSG_DELAY) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
context
|
||||
.set_config_internal(Config::BackupTransferMsgId, None)
|
||||
.await?;
|
||||
context
|
||||
.set_config_internal(Config::BackupTransferTimestamp, None)
|
||||
.await?;
|
||||
if Message::load_from_db_optional(context, MsgId::new(msg_id))
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
let mut msg = Message::new_text(backup_transfer_msg_body(context));
|
||||
add_device_msg_with_importance(context, None, Some(&mut msg), false, timestamp).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Provide or send a backup of this device.
|
||||
///
|
||||
/// This creates a backup of the current device and starts a service which offers another
|
||||
@@ -212,8 +259,23 @@ impl BackupProvider {
|
||||
info!(context, "Received backup reception acknowledgement.");
|
||||
context.emit_event(EventType::ImexProgress(1000));
|
||||
|
||||
let timestamp = time();
|
||||
let mut msg = Message::new_text(backup_transfer_msg_body(&context));
|
||||
add_device_msg(&context, None, Some(&mut msg)).await?;
|
||||
let msg_id =
|
||||
add_device_msg_with_importance(&context, None, Some(&mut msg), false, timestamp)
|
||||
.await?;
|
||||
context
|
||||
.set_config_internal(
|
||||
Config::BackupTransferTimestamp,
|
||||
Some(×tamp.to_string()),
|
||||
)
|
||||
.await?;
|
||||
context
|
||||
.set_config_internal(
|
||||
Config::BackupTransferMsgId,
|
||||
Some(&msg_id.to_u32().to_string()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -324,6 +386,7 @@ pub async fn get_backup2(
|
||||
info!(context, "Sending backup authentication token.");
|
||||
send_stream.write_all(auth_token.as_bytes()).await?;
|
||||
|
||||
let passphrase = String::new();
|
||||
info!(context, "Starting to read backup from the stream.");
|
||||
|
||||
let mut file_size_buf = [0u8; 8];
|
||||
@@ -333,7 +396,7 @@ pub async fn get_backup2(
|
||||
// Emit a nonzero progress so that UIs can display smth like "Transferring...".
|
||||
context.emit_event(EventType::ImexProgress(1));
|
||||
|
||||
import_backup_stream(context, recv_stream, file_size)
|
||||
import_backup_stream(context, recv_stream, file_size, passphrase)
|
||||
.await
|
||||
.context("Failed to import backup from QUIC stream")?;
|
||||
info!(context, "Finished importing backup from the stream.");
|
||||
@@ -392,14 +455,17 @@ pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::chat::{ChatItem, get_chat_msgs, send_msg};
|
||||
use crate::message::Viewtype;
|
||||
use crate::chat::{ChatId, ChatItem, get_chat_msgs, send_msg};
|
||||
use crate::contact::ContactId;
|
||||
use crate::message::{Viewtype, delete_msgs};
|
||||
use crate::sql::housekeeping;
|
||||
use crate::test_utils::TestContextManager;
|
||||
use crate::tools::SystemTime;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_receive() {
|
||||
async fn test_send_receive() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
// Create first device.
|
||||
@@ -471,6 +537,31 @@ mod tests {
|
||||
.get_matching(|ev| matches!(ev, EventType::ImexProgress(1000)))
|
||||
.await;
|
||||
}
|
||||
|
||||
// When deleting the backup transfer message on the sending device,
|
||||
// it is re-added once on housekeeping,
|
||||
// but not within the first hour after the transfer.
|
||||
let device_chat_id = ChatId::get_for_contact(&ctx0, ContactId::DEVICE).await?;
|
||||
let original_msg = ctx0.get_last_msg_in(device_chat_id).await;
|
||||
assert_eq!(original_msg.text, backup_transfer_msg_body(&ctx0));
|
||||
delete_msgs(&ctx0, &[original_msg.id]).await?;
|
||||
housekeeping(&ctx0).await?;
|
||||
assert!(get_chat_msgs(&ctx0, device_chat_id).await?.is_empty()); // re-adding not done in first hour
|
||||
|
||||
SystemTime::shift(Duration::from_secs(60 * 60));
|
||||
housekeeping(&ctx0).await?;
|
||||
let readded_msg = ctx0.get_last_msg_in(device_chat_id).await;
|
||||
assert_ne!(readded_msg.id, original_msg.id);
|
||||
assert_eq!(readded_msg.get_text(), original_msg.get_text());
|
||||
assert_eq!(readded_msg.get_timestamp(), original_msg.get_timestamp());
|
||||
assert!(readded_msg.get_sort_timestamp() > original_msg.get_sort_timestamp());
|
||||
|
||||
delete_msgs(&ctx0, &[readded_msg.id]).await?;
|
||||
housekeeping(&ctx0).await?;
|
||||
let device_msgs = get_chat_msgs(&ctx0, device_chat_id).await?;
|
||||
assert!(device_msgs.is_empty()); // re-adding is done only once
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that trying to accidentally overwrite a profile
|
||||
|
||||
+2
-1
@@ -98,7 +98,8 @@ pub trait DcKey: Serialize + Deserializable + Clone {
|
||||
// Because we write to a Vec<u8> the io::Write impls never
|
||||
// fail and we can hide this error.
|
||||
let mut buf = Vec::new();
|
||||
self.to_writer(&mut buf).unwrap();
|
||||
self.to_writer(&mut buf)
|
||||
.expect("Writing to Vec<u8> cannot fail");
|
||||
buf
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -52,7 +52,7 @@ const KEYUPDATE_CHUNK_CONTACTS: usize = 200;
|
||||
const KEYUPDATE_MAX_SILENCE: i64 = 3 * 365 * 24 * 3600;
|
||||
|
||||
/// Upper bound on the contacts informed after a relay list change, keeping the freshest.
|
||||
const KEYUPDATE_MAX_RECIPIENTS: u32 = 5000;
|
||||
const KEYUPDATE_MAX_RECIPIENTS: usize = 5000;
|
||||
|
||||
/// A contact to inform: the relays to reach them at, and the key to encrypt to.
|
||||
struct KeyupdateRecipient {
|
||||
@@ -63,7 +63,7 @@ struct KeyupdateRecipient {
|
||||
/// Returns at most `max_recipients` key-contacts to inform.
|
||||
async fn keyupdate_recipients(
|
||||
context: &Context,
|
||||
max_recipients: u32,
|
||||
max_recipients: usize,
|
||||
) -> Result<Vec<KeyupdateRecipient>> {
|
||||
// Single chat contacts only become keyupdate recipient candidates
|
||||
// if we have a record of a sent message or `last_seen` is not 0.
|
||||
|
||||
+2
-1
@@ -15,7 +15,8 @@
|
||||
clippy::explicit_iter_loop,
|
||||
clippy::explicit_into_iter_loop,
|
||||
clippy::cloned_instead_of_copied,
|
||||
clippy::manual_is_variant_and
|
||||
clippy::manual_is_variant_and,
|
||||
clippy::unnecessary_wraps
|
||||
)]
|
||||
#![cfg_attr(not(test), warn(clippy::arithmetic_side_effects))]
|
||||
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
|
||||
|
||||
+13
-8
@@ -580,7 +580,7 @@ impl Message {
|
||||
|
||||
if let Some(msg) = &mut msg {
|
||||
msg.additional_text =
|
||||
Self::get_additional_text(context, msg.download_state, &msg.param)?;
|
||||
Self::get_additional_text(context, msg.download_state, &msg.param);
|
||||
}
|
||||
|
||||
Ok(msg)
|
||||
@@ -618,7 +618,7 @@ impl Message {
|
||||
context: &Context,
|
||||
download_state: DownloadState,
|
||||
param: &Params,
|
||||
) -> Result<String> {
|
||||
) -> String {
|
||||
if download_state != DownloadState::Done {
|
||||
let file_size = param
|
||||
.get(Param::PostMessageFileBytes)
|
||||
@@ -635,14 +635,14 @@ impl Message {
|
||||
.unwrap_or("?".to_owned());
|
||||
|
||||
return match viewtype {
|
||||
Viewtype::File => Ok(format!(" [{file_name} – {file_size}]")),
|
||||
Viewtype::File => format!(" [{file_name} – {file_size}]"),
|
||||
_ => {
|
||||
let translated_viewtype = viewtype.to_locale_string(context);
|
||||
Ok(format!(" [{translated_viewtype} – {file_size}]"))
|
||||
format!(" [{translated_viewtype} – {file_size}]")
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(String::new())
|
||||
String::new()
|
||||
}
|
||||
|
||||
/// Returns the MIME type of an attached file if it exists.
|
||||
@@ -2017,14 +2017,19 @@ pub(crate) async fn set_msg_failed(
|
||||
|
||||
/// Inserts a tombstone into `msgs` table
|
||||
/// to prevent downloading the same message in the future.
|
||||
/// With `on_server`, copies seen on IMAP later are deleted on the server.
|
||||
///
|
||||
/// Returns tombstone database row ID.
|
||||
pub(crate) async fn insert_tombstone(context: &Context, rfc724_mid: &str) -> Result<MsgId> {
|
||||
pub(crate) async fn insert_tombstone(
|
||||
context: &Context,
|
||||
rfc724_mid: &str,
|
||||
on_server: bool,
|
||||
) -> Result<MsgId> {
|
||||
let row_id = context
|
||||
.sql
|
||||
.insert(
|
||||
"INSERT INTO msgs(rfc724_mid, chat_id) VALUES (?,?)",
|
||||
(rfc724_mid, ChatId::TRASH),
|
||||
"INSERT INTO msgs(rfc724_mid, chat_id, deleted) VALUES (?,?,?)",
|
||||
(rfc724_mid, ChatId::TRASH, on_server),
|
||||
)
|
||||
.await?;
|
||||
let msg_id = MsgId::new(u32::try_from(row_id)?);
|
||||
|
||||
@@ -784,7 +784,7 @@ async fn test_get_existing_msg_ids() -> Result<()> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_can_fail() -> Result<()> {
|
||||
fn test_can_fail() {
|
||||
use MessageState::*;
|
||||
|
||||
// states that are not allowed to transition to OutFailed
|
||||
@@ -799,6 +799,4 @@ fn test_can_fail() -> Result<()> {
|
||||
assert!(OutPending.can_fail());
|
||||
assert!(OutDelivered.can_fail());
|
||||
assert!(OutFailed.can_fail());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+339
-413
@@ -26,7 +26,6 @@ use crate::download::PostMsgMetadata;
|
||||
use crate::ensure_and_debug_assert;
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::key;
|
||||
use crate::key::{DcKey, SignedPublicKey, SignedSecretKey, load_self_public_key, self_fingerprint};
|
||||
use crate::location;
|
||||
use crate::log::warn;
|
||||
@@ -57,19 +56,6 @@ use crate::webxdc::StatusUpdateSerial;
|
||||
/// and divide by 4/3 to account for base64 encoding.
|
||||
pub const RECOMMENDED_FILE_SIZE: u64 = (30 - 1) * 1024 * 1024 / 4 * 3;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[expect(clippy::large_enum_variant)]
|
||||
pub enum Loaded {
|
||||
Message {
|
||||
chat: Chat,
|
||||
msg: Message,
|
||||
},
|
||||
Mdn {
|
||||
rfc724_mid: String,
|
||||
additional_msg_ids: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum PreMessageMode {
|
||||
/// adds the Chat-Is-Post-Message header in unprotected part
|
||||
@@ -185,7 +171,10 @@ pub struct MimeFactory {
|
||||
member_timestamps: Vec<i64>,
|
||||
|
||||
timestamp: i64,
|
||||
loaded: Loaded,
|
||||
|
||||
chat: Chat,
|
||||
msg: Message,
|
||||
|
||||
in_reply_to: String,
|
||||
|
||||
/// List of Message-IDs for `References` header.
|
||||
@@ -453,8 +442,8 @@ pub(crate) async fn render_queued_mail_with_context(
|
||||
context: &Context,
|
||||
) -> Result<RenderedEmail> {
|
||||
let from_addr = context.get_primary_self_addr().await?;
|
||||
let public_key = key::load_self_public_key(context).await?;
|
||||
let secret_key = key::load_self_secret_key(context).await?;
|
||||
let public_key = crate::key::load_self_public_key(context).await?;
|
||||
let secret_key = crate::key::load_self_secret_key(context).await?;
|
||||
|
||||
let rendered_mail = render_queued_mail(queued_mail, &public_key, &secret_key, from_addr)?;
|
||||
Ok(rendered_mail)
|
||||
@@ -511,11 +500,6 @@ impl MimeFactory {
|
||||
let mut member_fingerprints = Vec::new();
|
||||
let mut member_timestamps = Vec::new();
|
||||
let mut recipient_ids = HashSet::new();
|
||||
let req_mdn = !chat.is_self_talk()
|
||||
&& !msg.is_system_message()
|
||||
&& msg.param.get_int(Param::Reaction).unwrap_or_default() == 0
|
||||
&& context.should_request_mdns().await?;
|
||||
|
||||
let self_fingerprint = self_fingerprint(context).await?;
|
||||
|
||||
let encryption = if chat.is_self_talk() {
|
||||
@@ -796,6 +780,12 @@ impl MimeFactory {
|
||||
}
|
||||
};
|
||||
|
||||
let req_mdn = encryption.is_encrypted()
|
||||
&& !chat.is_self_talk()
|
||||
&& !msg.is_system_message()
|
||||
&& msg.param.get_int(Param::Reaction).unwrap_or_default() == 0
|
||||
&& context.should_request_mdns().await?;
|
||||
|
||||
let (in_reply_to, references) = context
|
||||
.sql
|
||||
.query_row(
|
||||
@@ -849,7 +839,8 @@ impl MimeFactory {
|
||||
member_fingerprints,
|
||||
member_timestamps,
|
||||
timestamp: msg.timestamp_sort,
|
||||
loaded: Loaded::Message { msg, chat },
|
||||
chat,
|
||||
msg,
|
||||
in_reply_to,
|
||||
references,
|
||||
req_mdn,
|
||||
@@ -860,62 +851,6 @@ impl MimeFactory {
|
||||
Ok(factory)
|
||||
}
|
||||
|
||||
pub async fn from_mdn(
|
||||
context: &Context,
|
||||
from_id: ContactId,
|
||||
rfc724_mid: String,
|
||||
additional_msg_ids: Vec<String>,
|
||||
) -> Result<MimeFactory> {
|
||||
let contact = Contact::get_by_id(context, from_id).await?;
|
||||
let from_addr = context.get_primary_self_addr().await?;
|
||||
let timestamp = time();
|
||||
|
||||
let addr = contact.get_addr().to_string();
|
||||
let mut recipients = vec![addr.clone()];
|
||||
|
||||
let encryption = if from_id == ContactId::SELF {
|
||||
Encryption::Asymmetric {
|
||||
encryption_pubkeys: Vec::new(),
|
||||
}
|
||||
} else if contact.is_key_contact() {
|
||||
let encryption_pubkeys = if let Some(key) = contact.public_key(context).await? {
|
||||
recipients = relay_addrs(&key, &addr);
|
||||
vec![(addr.clone(), key)]
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
Encryption::Asymmetric { encryption_pubkeys }
|
||||
} else {
|
||||
Encryption::No
|
||||
};
|
||||
|
||||
let res = MimeFactory {
|
||||
from_addr,
|
||||
from_displayname: "".to_string(),
|
||||
sender_displayname: None,
|
||||
selfstatus: "".to_string(),
|
||||
recipients,
|
||||
encryption,
|
||||
to: vec![("".to_string(), contact.get_addr().to_string())],
|
||||
past_members: vec![],
|
||||
member_fingerprints: vec![],
|
||||
member_timestamps: vec![],
|
||||
timestamp,
|
||||
loaded: Loaded::Mdn {
|
||||
rfc724_mid,
|
||||
additional_msg_ids,
|
||||
},
|
||||
in_reply_to: String::default(),
|
||||
references: Vec::new(),
|
||||
req_mdn: false,
|
||||
attach_selfavatar: false,
|
||||
webxdc_topic: None,
|
||||
pre_message_mode: PreMessageMode::None,
|
||||
};
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Returns whether own Autocrypt key should be attached to this MDN
|
||||
/// and if so, records the attachment.
|
||||
///
|
||||
@@ -925,15 +860,15 @@ impl MimeFactory {
|
||||
/// so that contacts we only read messages from
|
||||
/// still learn our current key and relay list
|
||||
/// and will likely re-gossip it to group chats.
|
||||
async fn update_mdn_pubkey_attachment(&self, context: &Context) -> Result<bool> {
|
||||
let Encryption::Asymmetric { encryption_pubkeys } = &self.encryption else {
|
||||
return Ok(false);
|
||||
};
|
||||
async fn update_mdn_pubkey_attachment(
|
||||
context: &Context,
|
||||
encryption_pubkeys: &[SignedPublicKey],
|
||||
) -> Result<bool> {
|
||||
debug_assert!(
|
||||
encryption_pubkeys.len() <= 1,
|
||||
"MDNs have at most one recipient key; own key is only added at encryption time"
|
||||
);
|
||||
let [(_, ref key)] = encryption_pubkeys[..] else {
|
||||
let [ref key] = encryption_pubkeys[..] else {
|
||||
return Ok(false);
|
||||
};
|
||||
let fingerprint = key.dc_fingerprint().hex();
|
||||
@@ -1015,80 +950,69 @@ impl MimeFactory {
|
||||
}
|
||||
|
||||
fn grpimage(&self) -> Option<String> {
|
||||
match &self.loaded {
|
||||
Loaded::Message { chat, msg } => {
|
||||
let cmd = msg.param.get_cmd();
|
||||
let cmd = self.msg.param.get_cmd();
|
||||
|
||||
match cmd {
|
||||
SystemMessage::MemberAddedToGroup => {
|
||||
return chat.param.get(Param::ProfileImage).map(Into::into);
|
||||
}
|
||||
SystemMessage::GroupImageChanged => {
|
||||
return msg.param.get(Param::Arg).map(Into::into);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if msg
|
||||
.param
|
||||
.get_bool(Param::AttachChatAvatarAndDescription)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
return chat.param.get(Param::ProfileImage).map(Into::into);
|
||||
}
|
||||
|
||||
None
|
||||
match cmd {
|
||||
SystemMessage::MemberAddedToGroup => {
|
||||
return self.chat.param.get(Param::ProfileImage).map(Into::into);
|
||||
}
|
||||
Loaded::Mdn { .. } => None,
|
||||
SystemMessage::GroupImageChanged => {
|
||||
return self.msg.param.get(Param::Arg).map(Into::into);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if self
|
||||
.msg
|
||||
.param
|
||||
.get_bool(Param::AttachChatAvatarAndDescription)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
return self.chat.param.get(Param::ProfileImage).map(Into::into);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
async fn subject_str(&self, context: &Context) -> Result<String> {
|
||||
let subject = match &self.loaded {
|
||||
Loaded::Message { chat, msg } => {
|
||||
let quoted_msg_subject = msg.quoted_message(context).await?.map(|m| m.subject);
|
||||
let quoted_msg_subject = self.msg.quoted_message(context).await?.map(|m| m.subject);
|
||||
|
||||
if !msg.subject.is_empty() {
|
||||
return Ok(msg.subject.clone());
|
||||
}
|
||||
if !self.msg.subject.is_empty() {
|
||||
return Ok(self.msg.subject.clone());
|
||||
}
|
||||
|
||||
if (chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast)
|
||||
&& quoted_msg_subject.is_none_or_empty()
|
||||
{
|
||||
let re = if self.in_reply_to.is_empty() {
|
||||
""
|
||||
} else {
|
||||
"Re: "
|
||||
};
|
||||
return Ok(format!("{}{}", re, chat.name));
|
||||
}
|
||||
if (self.chat.typ == Chattype::Group || self.chat.typ == Chattype::OutBroadcast)
|
||||
&& quoted_msg_subject.is_none_or_empty()
|
||||
{
|
||||
let re = if self.in_reply_to.is_empty() {
|
||||
""
|
||||
} else {
|
||||
"Re: "
|
||||
};
|
||||
return Ok(format!("{}{}", re, self.chat.name));
|
||||
}
|
||||
|
||||
let parent_subject = if quoted_msg_subject.is_none_or_empty() {
|
||||
chat.param.get(Param::LastSubject)
|
||||
} else {
|
||||
quoted_msg_subject.as_deref()
|
||||
};
|
||||
if let Some(last_subject) = parent_subject {
|
||||
return Ok(format!("Re: {}", remove_subject_prefix(last_subject)));
|
||||
}
|
||||
|
||||
let self_name = match Self::should_attach_profile_data(msg) {
|
||||
true => context.get_config(Config::Displayname).await?,
|
||||
false => None,
|
||||
};
|
||||
let self_name = &match self_name {
|
||||
Some(name) => name,
|
||||
None => context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
stock_str::subject_for_new_contact(context, self_name)
|
||||
}
|
||||
Loaded::Mdn { .. } => "Receipt Notification".to_string(), // untranslated to no reveal sender's language
|
||||
let parent_subject = if quoted_msg_subject.is_none_or_empty() {
|
||||
self.chat.param.get(Param::LastSubject)
|
||||
} else {
|
||||
quoted_msg_subject.as_deref()
|
||||
};
|
||||
if let Some(last_subject) = parent_subject {
|
||||
return Ok(format!("Re: {}", remove_subject_prefix(last_subject)));
|
||||
}
|
||||
|
||||
Ok(subject)
|
||||
let self_name = match Self::should_attach_profile_data(&self.msg) {
|
||||
true => context.get_config(Config::Displayname).await?,
|
||||
false => None,
|
||||
};
|
||||
let self_name = &match self_name {
|
||||
Some(name) => name,
|
||||
None => context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
Ok(stock_str::subject_for_new_contact(context, self_name))
|
||||
}
|
||||
|
||||
pub fn recipients(&self) -> Vec<String> {
|
||||
@@ -1165,10 +1089,10 @@ impl MimeFactory {
|
||||
));
|
||||
}
|
||||
|
||||
if let Loaded::Message { chat, .. } = &self.loaded
|
||||
&& chat.typ == Chattype::Group
|
||||
{
|
||||
if !self.member_timestamps.is_empty() && !chat.member_list_is_stale(context).await? {
|
||||
if self.chat.typ == Chattype::Group {
|
||||
if !self.member_timestamps.is_empty()
|
||||
&& !self.chat.member_list_is_stale(context).await?
|
||||
{
|
||||
headers.push((
|
||||
"Chat-Group-Member-Timestamps",
|
||||
mail_builder::headers::raw::Raw::new(
|
||||
@@ -1220,29 +1144,25 @@ impl MimeFactory {
|
||||
}
|
||||
|
||||
// Automatic Response headers <https://www.rfc-editor.org/rfc/rfc3834>
|
||||
if let Loaded::Mdn { .. } = self.loaded {
|
||||
headers.push((
|
||||
"Auto-Submitted",
|
||||
mail_builder::headers::raw::Raw::new("auto-replied".to_string()).into(),
|
||||
));
|
||||
} else if context.get_config_bool(Config::Bot).await? {
|
||||
if context.get_config_bool(Config::Bot).await? {
|
||||
headers.push((
|
||||
"Auto-Submitted",
|
||||
mail_builder::headers::raw::Raw::new("auto-generated".to_string()).into(),
|
||||
));
|
||||
}
|
||||
|
||||
if let Loaded::Message { msg, chat } = &self.loaded
|
||||
&& (chat.typ == Chattype::OutBroadcast || chat.typ == Chattype::InBroadcast)
|
||||
{
|
||||
if self.chat.typ == Chattype::OutBroadcast || self.chat.typ == Chattype::InBroadcast {
|
||||
headers.push((
|
||||
"Chat-List-ID",
|
||||
mail_builder::headers::text::Text::new(format!("{} <{}>", chat.name, chat.grpid))
|
||||
.into(),
|
||||
mail_builder::headers::text::Text::new(format!(
|
||||
"{} <{}>",
|
||||
self.chat.name, self.chat.grpid
|
||||
))
|
||||
.into(),
|
||||
));
|
||||
|
||||
if msg.param.get_cmd() == SystemMessage::MemberAddedToGroup
|
||||
&& let Some(secret) = msg.param.get(PARAM_BROADCAST_SECRET)
|
||||
if self.msg.param.get_cmd() == SystemMessage::MemberAddedToGroup
|
||||
&& let Some(secret) = self.msg.param.get(PARAM_BROADCAST_SECRET)
|
||||
{
|
||||
headers.push((
|
||||
"Chat-Broadcast-Secret",
|
||||
@@ -1251,22 +1171,18 @@ impl MimeFactory {
|
||||
}
|
||||
}
|
||||
|
||||
if let Loaded::Message { msg, .. } = &self.loaded {
|
||||
if let Some(original_rfc724_mid) = msg.param.get(Param::TextEditFor) {
|
||||
headers.push((
|
||||
"Chat-Edit",
|
||||
mail_builder::headers::message_id::MessageId::new(
|
||||
original_rfc724_mid.to_string(),
|
||||
)
|
||||
if let Some(original_rfc724_mid) = self.msg.param.get(Param::TextEditFor) {
|
||||
headers.push((
|
||||
"Chat-Edit",
|
||||
mail_builder::headers::message_id::MessageId::new(original_rfc724_mid.to_string())
|
||||
.into(),
|
||||
));
|
||||
} else if let Some(rfc724_mid_list) = msg.param.get(Param::DeleteRequestFor) {
|
||||
headers.push((
|
||||
"Chat-Delete",
|
||||
mail_builder::headers::message_id::MessageId::new(rfc724_mid_list.to_string())
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
));
|
||||
} else if let Some(rfc724_mid_list) = self.msg.param.get(Param::DeleteRequestFor) {
|
||||
headers.push((
|
||||
"Chat-Delete",
|
||||
mail_builder::headers::message_id::MessageId::new(rfc724_mid_list.to_string())
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
|
||||
headers.push((
|
||||
@@ -1303,14 +1219,12 @@ impl MimeFactory {
|
||||
// Add ephemeral timer for non-MDN messages.
|
||||
// For MDNs it does not matter because they are not visible
|
||||
// and ignored by the receiver.
|
||||
if let Loaded::Message { msg, .. } = &self.loaded {
|
||||
let ephemeral_timer = msg.chat_id.get_ephemeral_timer(context).await?;
|
||||
if let EphemeralTimer::Enabled { duration } = ephemeral_timer {
|
||||
headers.push((
|
||||
"Ephemeral-Timer",
|
||||
mail_builder::headers::raw::Raw::new(duration.to_string()).into(),
|
||||
));
|
||||
}
|
||||
let ephemeral_timer = self.msg.chat_id.get_ephemeral_timer(context).await?;
|
||||
if let EphemeralTimer::Enabled { duration } = ephemeral_timer {
|
||||
headers.push((
|
||||
"Ephemeral-Timer",
|
||||
mail_builder::headers::raw::Raw::new(duration.to_string()).into(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(headers)
|
||||
@@ -1318,19 +1232,14 @@ impl MimeFactory {
|
||||
|
||||
/// Helper function render the messages that are not queued.
|
||||
///
|
||||
/// Used for MDNs because they are fully rendered and sent in one go,
|
||||
/// rather than first creating a [`QueuedMail`] and sending it later.
|
||||
pub async fn render(self, context: &Context, from_addr: &str) -> Result<RenderedEmail> {
|
||||
/// Used only for tests.
|
||||
#[cfg(test)]
|
||||
pub async fn render(self, context: &Context) -> Result<RenderedEmail> {
|
||||
// Does not matter, we are not going to return the QueuedMail.
|
||||
let bcc_self = false;
|
||||
let public_key = key::load_self_public_key(context).await?;
|
||||
let secret_key = key::load_self_secret_key(context).await?;
|
||||
|
||||
let (queued_mail, _side_effects) =
|
||||
Box::pin(self.into_queued_mail(context, bcc_self)).await?;
|
||||
let rendered_mail =
|
||||
render_queued_mail(queued_mail, &public_key, &secret_key, from_addr.to_string())?;
|
||||
Ok(rendered_mail)
|
||||
render_queued_mail_with_context(queued_mail, context).await
|
||||
}
|
||||
|
||||
/// Consumes a `MimeFactory` and renders it into a message which is then stored in
|
||||
@@ -1341,18 +1250,15 @@ impl MimeFactory {
|
||||
context: &Context,
|
||||
bcc_self: bool,
|
||||
) -> Result<ToBeQueuedMail> {
|
||||
let rfc724_mid = match &self.loaded {
|
||||
Loaded::Message { msg, .. } => match &self.pre_message_mode {
|
||||
PreMessageMode::Pre { .. } => {
|
||||
if msg.pre_rfc724_mid.is_empty() {
|
||||
create_outgoing_rfc724_mid()
|
||||
} else {
|
||||
msg.pre_rfc724_mid.clone()
|
||||
}
|
||||
let rfc724_mid = match &self.pre_message_mode {
|
||||
PreMessageMode::Pre { .. } => {
|
||||
if self.msg.pre_rfc724_mid.is_empty() {
|
||||
create_outgoing_rfc724_mid()
|
||||
} else {
|
||||
self.msg.pre_rfc724_mid.clone()
|
||||
}
|
||||
_ => msg.rfc724_mid.clone(),
|
||||
},
|
||||
Loaded::Mdn { .. } => create_outgoing_rfc724_mid(),
|
||||
}
|
||||
_ => self.msg.rfc724_mid.clone(),
|
||||
};
|
||||
|
||||
let subject_str = self.subject_str(context).await?;
|
||||
@@ -1362,61 +1268,44 @@ impl MimeFactory {
|
||||
|
||||
let is_encrypted = self.will_be_encrypted();
|
||||
|
||||
let side_effects: Option<QueueSideEffects>;
|
||||
let RenderedMessage {
|
||||
main_part,
|
||||
mut parts,
|
||||
last_added_location_timestamp,
|
||||
avatar_is_attached,
|
||||
sync_ids_to_delete,
|
||||
} = self
|
||||
.render_message(context, &mut headers, &grpimage, is_encrypted)
|
||||
.await?;
|
||||
|
||||
let message: MimePart<'static> = match &self.loaded {
|
||||
Loaded::Message { msg, .. } => {
|
||||
let msg = msg.clone();
|
||||
let RenderedMessage {
|
||||
main_part,
|
||||
mut parts,
|
||||
last_added_location_timestamp,
|
||||
avatar_is_attached,
|
||||
sync_ids_to_delete,
|
||||
} = self
|
||||
.render_message(context, &mut headers, &grpimage, is_encrypted)
|
||||
.await?;
|
||||
let side_effects = QueueSideEffects {
|
||||
chat_id: self.msg.chat_id,
|
||||
avatar_is_attached,
|
||||
sync_ids_to_delete,
|
||||
last_added_location_timestamp,
|
||||
subject: subject_str,
|
||||
};
|
||||
|
||||
side_effects = Some(QueueSideEffects {
|
||||
chat_id: msg.chat_id,
|
||||
avatar_is_attached,
|
||||
sync_ids_to_delete,
|
||||
last_added_location_timestamp,
|
||||
subject: subject_str,
|
||||
});
|
||||
let message: MimePart<'static> = if parts.is_empty() {
|
||||
// Single part, render as regular message.
|
||||
main_part
|
||||
} else {
|
||||
parts.insert(0, main_part);
|
||||
|
||||
if parts.is_empty() {
|
||||
// Single part, render as regular message.
|
||||
main_part
|
||||
} else {
|
||||
parts.insert(0, main_part);
|
||||
|
||||
// Multiple parts, render as multipart.
|
||||
if msg.param.get_cmd() == SystemMessage::MultiDeviceSync {
|
||||
MimePart::new("multipart/report; report-type=multi-device-sync", parts)
|
||||
} else if msg.param.get_cmd() == SystemMessage::WebxdcStatusUpdate {
|
||||
MimePart::new("multipart/report; report-type=status-update", parts)
|
||||
} else {
|
||||
MimePart::new("multipart/mixed", parts)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loaded::Mdn { .. } => {
|
||||
side_effects = None;
|
||||
self.render_mdn()?
|
||||
// Multiple parts, render as multipart.
|
||||
if self.msg.param.get_cmd() == SystemMessage::MultiDeviceSync {
|
||||
MimePart::new("multipart/report; report-type=multi-device-sync", parts)
|
||||
} else if self.msg.param.get_cmd() == SystemMessage::WebxdcStatusUpdate {
|
||||
MimePart::new("multipart/report; report-type=status-update", parts)
|
||||
} else {
|
||||
MimePart::new("multipart/mixed", parts)
|
||||
}
|
||||
};
|
||||
|
||||
let should_attach_pubkey = match &self.loaded {
|
||||
Loaded::Message { .. } => true,
|
||||
Loaded::Mdn { .. } => self.update_mdn_pubkey_attachment(context).await?,
|
||||
};
|
||||
let should_attach_pubkey = true;
|
||||
let is_post_message = self.pre_message_mode == PreMessageMode::Post;
|
||||
|
||||
let is_securejoin_message = match &self.loaded {
|
||||
Loaded::Message { msg, .. } => msg.param.get_cmd() == SystemMessage::SecurejoinMessage,
|
||||
Loaded::Mdn { .. } => false,
|
||||
};
|
||||
let is_securejoin_message = self.msg.param.get_cmd() == SystemMessage::SecurejoinMessage;
|
||||
|
||||
// Disable compression for SecureJoin to ensure
|
||||
// there are no compression side channels
|
||||
@@ -1434,79 +1323,70 @@ impl MimeFactory {
|
||||
let gossip_period = context.get_config_i64(Config::GossipPeriod).await?;
|
||||
let now = time();
|
||||
|
||||
match &self.loaded {
|
||||
Loaded::Message { chat, msg } => {
|
||||
if !should_hide_recipients(msg, chat) {
|
||||
for (addr, key) in encryption_pubkeys {
|
||||
let fingerprint = key.dc_fingerprint().hex();
|
||||
let cmd = msg.param.get_cmd();
|
||||
if is_post_message {
|
||||
continue;
|
||||
}
|
||||
if !should_hide_recipients(&self.msg, &self.chat) {
|
||||
for (addr, key) in encryption_pubkeys {
|
||||
let fingerprint = key.dc_fingerprint().hex();
|
||||
let cmd = self.msg.param.get_cmd();
|
||||
if is_post_message {
|
||||
continue;
|
||||
}
|
||||
|
||||
let should_do_gossip = cmd == SystemMessage::MemberAddedToGroup
|
||||
|| cmd == SystemMessage::SecurejoinMessage
|
||||
|| multiple_recipients && {
|
||||
let gossiped_timestamp: Option<i64> = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT timestamp
|
||||
let should_do_gossip = cmd == SystemMessage::MemberAddedToGroup
|
||||
|| cmd == SystemMessage::SecurejoinMessage
|
||||
|| multiple_recipients && {
|
||||
let gossiped_timestamp: Option<i64> = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT timestamp
|
||||
FROM gossip_timestamp
|
||||
WHERE chat_id=? AND fingerprint=?",
|
||||
(chat.id, &fingerprint),
|
||||
)
|
||||
.await?;
|
||||
(self.chat.id, &fingerprint),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// `gossip_period == 0` is a special case for testing,
|
||||
// enabling gossip in every message.
|
||||
//
|
||||
// If current time is in the past compared to
|
||||
// `gossiped_timestamp`, we also gossip because
|
||||
// either the `gossiped_timestamp` or clock is wrong.
|
||||
gossip_period == 0
|
||||
|| gossiped_timestamp
|
||||
.is_none_or(|ts| now >= ts + gossip_period || now < ts)
|
||||
};
|
||||
// `gossip_period == 0` is a special case for testing,
|
||||
// enabling gossip in every message.
|
||||
//
|
||||
// If current time is in the past compared to
|
||||
// `gossiped_timestamp`, we also gossip because
|
||||
// either the `gossiped_timestamp` or clock is wrong.
|
||||
gossip_period == 0
|
||||
|| gossiped_timestamp
|
||||
.is_none_or(|ts| now >= ts + gossip_period || now < ts)
|
||||
};
|
||||
|
||||
if !should_do_gossip {
|
||||
continue;
|
||||
}
|
||||
if !should_do_gossip {
|
||||
continue;
|
||||
}
|
||||
|
||||
let header = Aheader {
|
||||
addr: addr.clone(),
|
||||
public_key: key.clone(),
|
||||
// Autocrypt 1.1.0 specification says that
|
||||
// `prefer-encrypt` attribute SHOULD NOT be included.
|
||||
prefer_encrypt: EncryptPreference::NoPreference,
|
||||
}
|
||||
.to_string();
|
||||
let header = Aheader {
|
||||
addr: addr.clone(),
|
||||
public_key: key.clone(),
|
||||
// Autocrypt 1.1.0 specification says that
|
||||
// `prefer-encrypt` attribute SHOULD NOT be included.
|
||||
prefer_encrypt: EncryptPreference::NoPreference,
|
||||
}
|
||||
.to_string();
|
||||
|
||||
headers.push((
|
||||
"Autocrypt-Gossip",
|
||||
mail_builder::headers::raw::Raw::new(header).into(),
|
||||
));
|
||||
headers.push((
|
||||
"Autocrypt-Gossip",
|
||||
mail_builder::headers::raw::Raw::new(header).into(),
|
||||
));
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO gossip_timestamp (chat_id, fingerprint, timestamp)
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO gossip_timestamp (chat_id, fingerprint, timestamp)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT (chat_id, fingerprint)
|
||||
DO UPDATE SET timestamp=excluded.timestamp",
|
||||
(chat.id, &fingerprint, now),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loaded::Mdn { .. } => {
|
||||
// Never gossip in MDNs.
|
||||
(self.chat.id, &fingerprint, now),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let is_encrypted = self.will_be_encrypted();
|
||||
|
||||
let display_name = if is_securejoin_message && !is_encrypted {
|
||||
// Unencrypted securejoin messages should _not_ include the display name.
|
||||
"".to_string()
|
||||
@@ -1514,25 +1394,13 @@ impl MimeFactory {
|
||||
self.from_displayname.clone()
|
||||
};
|
||||
|
||||
let is_mdn = matches!(self.loaded, Loaded::Mdn { .. });
|
||||
let should_sign = true;
|
||||
|
||||
let message = if is_encrypted {
|
||||
add_headers_to_encrypted_part(message, headers)
|
||||
} else if is_mdn {
|
||||
// Never add outer multipart/mixed wrapper to MDN
|
||||
// as multipart/report Content-Type is used to recognize MDNs
|
||||
// by Delta Chat receiver and Chatmail servers
|
||||
// allowing them to be unencrypted and not contain Autocrypt header
|
||||
// without resetting Autocrypt encryption or triggering Chatmail filter
|
||||
// that normally only allows encrypted mails.
|
||||
message
|
||||
} else {
|
||||
// Unencrypted message.
|
||||
let message = if let Loaded::Message { msg, .. } = &self.loaded
|
||||
&& msg.param.get_cmd() == SystemMessage::SecurejoinMessage
|
||||
let message = if self.msg.param.get_cmd() == SystemMessage::SecurejoinMessage
|
||||
&& matches!(
|
||||
msg.param.get(Param::Arg),
|
||||
self.msg.param.get(Param::Arg),
|
||||
Some("vc-request") | Some("vg-request")
|
||||
) {
|
||||
// Workaround for legacy SecureJoin {vc,vg}-request messages.
|
||||
@@ -1545,12 +1413,7 @@ impl MimeFactory {
|
||||
message
|
||||
};
|
||||
|
||||
headers.iter().fold(message, |message, (header, value)| {
|
||||
debug_assert_ne!(*header, "from");
|
||||
debug_assert_ne!(*header, "message-id");
|
||||
debug_assert_ne!(*header, "autocrypt");
|
||||
message.header(*header, value.clone())
|
||||
})
|
||||
add_headers_to_part(message, headers)
|
||||
};
|
||||
let raw_message = part_to_bytes(message);
|
||||
let recipients = self.recipients();
|
||||
@@ -1561,7 +1424,7 @@ impl MimeFactory {
|
||||
display_name,
|
||||
encryption: self.encryption.into_queued_encryption(),
|
||||
should_attach_pubkey,
|
||||
should_sign,
|
||||
should_sign: true,
|
||||
should_compress,
|
||||
recipients,
|
||||
sent_to: Vec::new(),
|
||||
@@ -1572,14 +1435,10 @@ impl MimeFactory {
|
||||
|
||||
/// Returns MIME part with a `message.kml` attachment.
|
||||
fn get_message_kml_part(&self) -> Option<MimePart<'static>> {
|
||||
let Loaded::Message { msg, .. } = &self.loaded else {
|
||||
return None;
|
||||
};
|
||||
let latitude = self.msg.param.get_float(Param::SetLatitude)?;
|
||||
let longitude = self.msg.param.get_float(Param::SetLongitude)?;
|
||||
|
||||
let latitude = msg.param.get_float(Param::SetLatitude)?;
|
||||
let longitude = msg.param.get_float(Param::SetLongitude)?;
|
||||
|
||||
let kml_file = location::get_message_kml(msg.timestamp_sort, latitude, longitude);
|
||||
let kml_file = location::get_message_kml(self.msg.timestamp_sort, latitude, longitude);
|
||||
let part = MimePart::new("application/vnd.google-earth.kml+xml", kml_file)
|
||||
.attachment("message.kml");
|
||||
Some(part)
|
||||
@@ -1591,12 +1450,8 @@ impl MimeFactory {
|
||||
&self,
|
||||
context: &Context,
|
||||
) -> Result<Option<(MimePart<'static>, i64)>> {
|
||||
let Loaded::Message { msg, .. } = &self.loaded else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let Some((kml_content, last_added_location_timestamp)) =
|
||||
location::get_kml(context, msg.chat_id).await?
|
||||
location::get_kml(context, self.msg.chat_id).await?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
@@ -1613,11 +1468,8 @@ impl MimeFactory {
|
||||
grpimage: &Option<String>,
|
||||
is_encrypted: bool,
|
||||
) -> Result<RenderedMessage> {
|
||||
let Loaded::Message { chat, msg } = &self.loaded else {
|
||||
bail!("Attempt to render MDN as a message");
|
||||
};
|
||||
let chat = chat.clone();
|
||||
let msg = msg.clone();
|
||||
let chat = self.chat.clone();
|
||||
let msg = self.msg.clone();
|
||||
let command = msg.param.get_cmd();
|
||||
let mut placeholdertext = None;
|
||||
|
||||
@@ -2153,59 +2005,6 @@ impl MimeFactory {
|
||||
})
|
||||
}
|
||||
|
||||
/// Render an MDN
|
||||
fn render_mdn(&mut self) -> Result<MimePart<'static>> {
|
||||
// RFC 6522, this also requires the `report-type` parameter which is equal
|
||||
// to the MIME subtype of the second body part of the multipart/report
|
||||
let Loaded::Mdn {
|
||||
rfc724_mid,
|
||||
additional_msg_ids,
|
||||
} = &self.loaded
|
||||
else {
|
||||
bail!("Attempt to render a message as MDN");
|
||||
};
|
||||
|
||||
// first body part: always human-readable, always REQUIRED by RFC 6522.
|
||||
// untranslated to no reveal sender's language.
|
||||
// moreover, translations in unknown languages are confusing, and clients may not display them at all
|
||||
let text_part = MimePart::new("text/plain", "This is a receipt notification.");
|
||||
|
||||
let mut message = MimePart::new(
|
||||
"multipart/report; report-type=disposition-notification",
|
||||
vec![text_part],
|
||||
);
|
||||
|
||||
// second body part: machine-readable, always REQUIRED by RFC 6522
|
||||
//
|
||||
// We do not include the Final-Recipient field.
|
||||
// According to <https://datatracker.ietf.org/doc/html/rfc8098#section-3.2.4>
|
||||
// it MUST be present and be the address on which original message was received,
|
||||
// but practically it is not going to be used.
|
||||
let message_text2 = format!(
|
||||
"Original-Message-ID: <{rfc724_mid}>\r\n\
|
||||
Disposition: manual-action/MDN-sent-automatically; displayed\r\n",
|
||||
);
|
||||
|
||||
let extension_fields = if additional_msg_ids.is_empty() {
|
||||
"".to_string()
|
||||
} else {
|
||||
"Additional-Message-IDs: ".to_string()
|
||||
+ &additional_msg_ids
|
||||
.iter()
|
||||
.map(|mid| render_rfc724_mid(mid))
|
||||
.collect::<Vec<String>>()
|
||||
.join(" ")
|
||||
+ "\r\n"
|
||||
};
|
||||
|
||||
message.add_part(MimePart::new(
|
||||
"message/disposition-notification",
|
||||
message_text2 + &extension_fields,
|
||||
));
|
||||
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
pub fn will_be_encrypted(&self) -> bool {
|
||||
self.encryption.is_encrypted()
|
||||
}
|
||||
@@ -2235,20 +2034,26 @@ pub(crate) fn wrap_encrypted_part(encrypted: String) -> MimePart<'static> {
|
||||
)
|
||||
}
|
||||
|
||||
fn add_headers_to_part(
|
||||
message: MimePart<'static>,
|
||||
headers: Vec<(&'static str, HeaderType<'static>)>,
|
||||
) -> MimePart<'static> {
|
||||
headers
|
||||
.into_iter()
|
||||
.fold(message, |message, (header, value)| {
|
||||
debug_assert_ne!(header, "from");
|
||||
debug_assert_ne!(header, "message-id");
|
||||
debug_assert_ne!(header, "autocrypt");
|
||||
message.header(header, value)
|
||||
})
|
||||
}
|
||||
|
||||
fn add_headers_to_encrypted_part(
|
||||
message: MimePart<'static>,
|
||||
protected_headers: Vec<(&'static str, HeaderType<'static>)>,
|
||||
) -> MimePart<'static> {
|
||||
// Store protected headers in the inner message.
|
||||
let mut message: MimePart<'static> =
|
||||
protected_headers
|
||||
.into_iter()
|
||||
.fold(message, |message, (header, value)| {
|
||||
debug_assert_ne!(header, "from");
|
||||
debug_assert_ne!(header, "message-id");
|
||||
debug_assert_ne!(header, "autocrypt");
|
||||
message.header(header, value)
|
||||
});
|
||||
let mut message = add_headers_to_part(message, protected_headers);
|
||||
|
||||
// Set the appropriate Content-Type for the inner message
|
||||
for (h, v) in &mut message.headers {
|
||||
@@ -2436,6 +2241,127 @@ pub(crate) async fn symm_encrypted_securejoin_message(
|
||||
Ok(queued_mail)
|
||||
}
|
||||
|
||||
/// Returns an MDN body.
|
||||
fn mdn_body(rfc724_mid: &str, additional_msg_ids: Vec<String>) -> MimePart<'static> {
|
||||
// RFC 6522, this also requires the `report-type` parameter which is equal
|
||||
// to the MIME subtype of the second body part of the multipart/report
|
||||
|
||||
// first body part: always human-readable, always REQUIRED by RFC 6522.
|
||||
// untranslated to no reveal sender's language.
|
||||
// moreover, translations in unknown languages are confusing, and clients may not display them at all
|
||||
let text_part = MimePart::new("text/plain", "This is a receipt notification.");
|
||||
|
||||
// second body part: machine-readable, always REQUIRED by RFC 6522
|
||||
//
|
||||
// We do not include the Final-Recipient field.
|
||||
// According to <https://datatracker.ietf.org/doc/html/rfc8098#section-3.2.4>
|
||||
// it MUST be present and be the address on which original message was received,
|
||||
// but practically it is not going to be used.
|
||||
let message_text2 = format!(
|
||||
"Original-Message-ID: <{rfc724_mid}>\r\n\
|
||||
Disposition: manual-action/MDN-sent-automatically; displayed\r\n",
|
||||
);
|
||||
|
||||
let extension_fields = if additional_msg_ids.is_empty() {
|
||||
"".to_string()
|
||||
} else {
|
||||
"Additional-Message-IDs: ".to_string()
|
||||
+ &additional_msg_ids
|
||||
.iter()
|
||||
.map(|mid| render_rfc724_mid(mid))
|
||||
.collect::<Vec<String>>()
|
||||
.join(" ")
|
||||
+ "\r\n"
|
||||
};
|
||||
|
||||
MimePart::new(
|
||||
"multipart/report; report-type=disposition-notification",
|
||||
vec![
|
||||
text_part,
|
||||
MimePart::new(
|
||||
"message/disposition-notification",
|
||||
message_text2 + &extension_fields,
|
||||
),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) async fn mdn(
|
||||
context: &Context,
|
||||
contact_id: ContactId,
|
||||
rfc724_mid: &str,
|
||||
additional_rfc724_mids: Vec<String>,
|
||||
) -> Result<QueuedMail> {
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
let timestamp = time();
|
||||
|
||||
let addr = contact.get_addr().to_string();
|
||||
let recipients: Vec<String>;
|
||||
let encryption_pubkeys = if contact_id == ContactId::SELF {
|
||||
recipients = Vec::new();
|
||||
Vec::new()
|
||||
} else if let Some(key) = contact.public_key(context).await? {
|
||||
recipients = relay_addrs(&key, &addr);
|
||||
vec![key]
|
||||
} else {
|
||||
warn!(context, "Contact {contact_id} has no key, sending to self.");
|
||||
// Encryption key for the contact is not available, sending MDN to self only.
|
||||
recipients = Vec::new();
|
||||
Vec::new()
|
||||
};
|
||||
let bcc_self = context.get_config_bool(Config::BccSelf).await?;
|
||||
|
||||
let date = chrono::DateTime::<chrono::Utc>::from_timestamp(timestamp, 0)
|
||||
.context("Failed to convert timestamp to DateTime")?
|
||||
.to_rfc2822();
|
||||
let headers: Vec<(&'static str, HeaderType<'static>)> = vec![
|
||||
("Date", mail_builder::headers::raw::Raw::new(date).into()),
|
||||
(
|
||||
"To",
|
||||
mail_builder::headers::address::Address::new_list(vec![Address::new_address(
|
||||
None::<&'static str>,
|
||||
contact.get_addr().to_string(),
|
||||
)])
|
||||
.into(),
|
||||
),
|
||||
// Subject is untranslated to not reveal sender's language.
|
||||
(
|
||||
"Subject",
|
||||
mail_builder::headers::text::Text::new("Receipt Notification".to_string()).into(),
|
||||
),
|
||||
// Automatic Response headers <https://www.rfc-editor.org/rfc/rfc3834>
|
||||
(
|
||||
"Auto-Submitted",
|
||||
mail_builder::headers::raw::Raw::new("auto-replied".to_string()).into(),
|
||||
),
|
||||
(
|
||||
"Chat-Version",
|
||||
mail_builder::headers::raw::Raw::new("1.0").into(),
|
||||
),
|
||||
];
|
||||
|
||||
let message = mdn_body(rfc724_mid, additional_rfc724_mids);
|
||||
let should_attach_pubkey =
|
||||
MimeFactory::update_mdn_pubkey_attachment(context, &encryption_pubkeys).await?;
|
||||
let message = add_headers_to_encrypted_part(message, headers);
|
||||
let raw_message = part_to_bytes(message);
|
||||
|
||||
let encryption = QueuedEncryption::Asymmetric { encryption_pubkeys };
|
||||
let queued_mdn = QueuedMail {
|
||||
raw_message,
|
||||
rfc724_mid: create_outgoing_rfc724_mid(),
|
||||
display_name: String::new(),
|
||||
encryption,
|
||||
should_attach_pubkey,
|
||||
should_sign: true,
|
||||
should_compress: true,
|
||||
recipients,
|
||||
sent_to: Vec::new(),
|
||||
bcc_self,
|
||||
};
|
||||
Ok(queued_mdn)
|
||||
}
|
||||
|
||||
/// Returns the body of a keyupdate message, shaped like a receipt notification.
|
||||
///
|
||||
/// The shape is what every core goes by, as a keyupdate carries no marker:
|
||||
@@ -2443,7 +2369,7 @@ pub(crate) async fn symm_encrypted_securejoin_message(
|
||||
/// while a plain text body would end up in a contact request.
|
||||
/// The report deliberately names no original message, see [`crate::keyupdate`].
|
||||
fn keyupdate_body() -> MimePart<'static> {
|
||||
// Human-readable first part as RFC 6522 requires, untranslated like in `render_mdn`.
|
||||
// Human-readable first part as RFC 6522 requires.
|
||||
let text_part = MimePart::new(
|
||||
"text/plain",
|
||||
"This message updates the sender's encryption key and relay list.",
|
||||
|
||||
@@ -281,8 +281,12 @@ async fn test_subject_mdn() {
|
||||
assert_eq!("Re: Hello, Bob", mf.subject_str(t).await.unwrap());
|
||||
}
|
||||
|
||||
/// Tests that MDN for unencrypted message can be created without throwing an error.
|
||||
///
|
||||
/// We do not send unencrypted MDNs, but do not want SMTP loop to get stuck
|
||||
/// if we somehow request the creation of unencrypted MDN.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_mdn_create_encrypted() -> Result<()> {
|
||||
async fn test_mdn_create_unencrypted() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
alice.allow_unencrypted().await?;
|
||||
@@ -297,21 +301,43 @@ async fn test_mdn_create_encrypted() -> Result<()> {
|
||||
.await?;
|
||||
bob.set_config_bool(Config::MdnsEnabled, true).await?;
|
||||
|
||||
// MDN for unencrypted message is not encrypted.
|
||||
// MDN for unencrypted message.
|
||||
// Should not happen, but should also not throw an error.
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
let chat_alice = alice.create_email_chat(&bob).await.id;
|
||||
let sent = alice.send_msg(chat_alice, &mut msg).await;
|
||||
|
||||
let rcvd = bob.recv_msg(&sent).await;
|
||||
message::markseen_msgs(&bob, vec![rcvd.id]).await?;
|
||||
let mimefactory =
|
||||
MimeFactory::from_mdn(&bob, rcvd.from_id, rcvd.rfc724_mid.clone(), vec![]).await?;
|
||||
assert!(!mimefactory.will_be_encrypted());
|
||||
let bob_addr = bob.get_primary_self_addr().await?;
|
||||
let rendered_msg = mimefactory.render(&bob, &bob_addr).await?;
|
||||
let queued_mdn = mdn(&bob, rcvd.from_id, &rcvd.rfc724_mid, vec![]).await?;
|
||||
|
||||
// Only sending MDN to self (if BCC-self is enabled) because address-contact recipient has no key.
|
||||
assert!(queued_mdn.recipients.is_empty());
|
||||
|
||||
// MDNs are always encrypted, even if requested for unencrypted message.
|
||||
assert!(queued_mdn.encryption.is_encrypted());
|
||||
|
||||
bob.assert_warn("has no key, sending to self").await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that MDNs sent in reply to encrypted messages are encrypted
|
||||
/// and MDNs for unencrypted messages are not created.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_mdn_create_encrypted() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
alice
|
||||
.set_config(Config::Displayname, Some("Alice Exampleorg"))
|
||||
.await?;
|
||||
let bob = tcm.bob().await;
|
||||
bob.set_config(Config::Displayname, Some("Bob Examplenet"))
|
||||
.await?;
|
||||
bob.set_config(Config::Selfstatus, Some("Bob Examplenet"))
|
||||
.await?;
|
||||
bob.set_config_bool(Config::MdnsEnabled, true).await?;
|
||||
|
||||
assert!(!rendered_msg.message.contains("Bob Examplenet"));
|
||||
assert!(!rendered_msg.message.contains("Alice Exampleorg"));
|
||||
let bob_alice_contact = bob.add_or_lookup_contact(&alice).await;
|
||||
assert_eq!(bob_alice_contact.get_authname(), "Alice Exampleorg");
|
||||
|
||||
@@ -319,10 +345,9 @@ async fn test_mdn_create_encrypted() -> Result<()> {
|
||||
let rcvd = tcm.send_recv(&alice, &bob, "Heyho").await;
|
||||
message::markseen_msgs(&bob, vec![rcvd.id]).await?;
|
||||
|
||||
let mimefactory = MimeFactory::from_mdn(&bob, rcvd.from_id, rcvd.rfc724_mid, vec![]).await?;
|
||||
assert!(mimefactory.will_be_encrypted());
|
||||
let bob_addr = bob.get_primary_self_addr().await?;
|
||||
let rendered_msg = mimefactory.render(&bob, &bob_addr).await?;
|
||||
let queued_mdn = mdn(&bob, rcvd.from_id, &rcvd.rfc724_mid, vec![]).await?;
|
||||
assert!(queued_mdn.encryption.is_encrypted());
|
||||
let rendered_msg = render_queued_mail_with_context(queued_mdn, &bob).await?;
|
||||
|
||||
assert!(!rendered_msg.message.contains("Bob Examplenet"));
|
||||
assert!(!rendered_msg.message.contains("Alice Exampleorg"));
|
||||
@@ -349,8 +374,8 @@ async fn test_mdn_sent_to_all_relays() -> Result<()> {
|
||||
)?;
|
||||
import_public_key(alice, &bob_public_key).await?;
|
||||
|
||||
let mimefactory = MimeFactory::from_mdn(alice, rcvd.from_id, rcvd.rfc724_mid, vec![]).await?;
|
||||
let mut recipients = mimefactory.recipients();
|
||||
let queued_mdn = mdn(alice, rcvd.from_id, &rcvd.rfc724_mid, vec![]).await?;
|
||||
let mut recipients = queued_mdn.recipients;
|
||||
recipients.sort();
|
||||
assert_eq!(recipients, vec!["bob@example.net", "bob@relay2.example"]);
|
||||
|
||||
@@ -364,9 +389,8 @@ async fn test_mdn_autocrypt_throttle() -> Result<()> {
|
||||
alice: &TestContext,
|
||||
rcvd: &Message,
|
||||
) -> Result<bool> {
|
||||
let mf = MimeFactory::from_mdn(bob, rcvd.from_id, rcvd.rfc724_mid.clone(), vec![]).await?;
|
||||
let addr = bob.get_primary_self_addr().await?;
|
||||
let rendered_msg = mf.render(bob, &addr).await?;
|
||||
let queued_mdn = mdn(bob, rcvd.from_id, &rcvd.rfc724_mid, vec![]).await?;
|
||||
let rendered_msg = render_queued_mail_with_context(queued_mdn, bob).await?;
|
||||
let mime = MimeMessage::from_bytes(alice, rendered_msg.message.as_bytes()).await?;
|
||||
Ok(mime.autocrypt_fingerprint.is_some())
|
||||
}
|
||||
@@ -647,8 +671,7 @@ async fn test_render_reply() {
|
||||
let recipients = mimefactory.recipients();
|
||||
assert_eq!(recipients, vec!["charlie@example.net"]);
|
||||
|
||||
let addr = t.get_primary_self_addr().await.unwrap();
|
||||
let rendered_msg = mimefactory.render(t, &addr).await.unwrap();
|
||||
let rendered_msg = mimefactory.render(t).await.unwrap();
|
||||
|
||||
let mail = mailparse::parse_mail(rendered_msg.message.as_bytes()).unwrap();
|
||||
assert_eq!(
|
||||
@@ -795,7 +818,7 @@ async fn test_protected_headers_directive() -> Result<()> {
|
||||
// Long messages are truncated and MimeMessage::decoded_data is set for them. We need
|
||||
// decoded_data to check presence of the necessary headers.
|
||||
msg.set_text("a".repeat(constants::DC_DESIRED_TEXT_LEN + 1));
|
||||
msg.set_file_from_bytes(&bob, "foo.bar", "content".as_bytes(), None)?;
|
||||
msg.set_file_from_bytes(&bob, "foo.bar", b"content", None)?;
|
||||
let sent = bob.send_msg(chat, &mut msg).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
assert!(sent.payload.contains("\r\nSubject: [...]\r\n"));
|
||||
@@ -1100,7 +1123,6 @@ To: <bob@example.net>
|
||||
Subject: Message from alice@example.org
|
||||
References: <MESSAGE_ID@localhost>
|
||||
Chat-Version: 1.0
|
||||
Chat-Disposition-Notification-To: alice@example.org
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
Hello!"#
|
||||
@@ -1127,7 +1149,7 @@ async fn test_render_unencrypted_msg_with_attachment() -> Result<()> {
|
||||
.await;
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_text("Hello!".to_string());
|
||||
msg.set_file_from_bytes(alice, "foo.bar", "content".as_bytes(), None)?;
|
||||
msg.set_file_from_bytes(alice, "foo.bar", b"content", None)?;
|
||||
let sent = alice.send_msg(chat.id, &mut msg).await;
|
||||
let unencrypted = normalized_payload(sent).await;
|
||||
|
||||
@@ -1152,7 +1174,6 @@ To: <bob@example.net>
|
||||
Subject: Message from alice@example.org
|
||||
References: <MESSAGE_ID@localhost>
|
||||
Chat-Version: 1.0
|
||||
Chat-Disposition-Notification-To: alice@example.org
|
||||
|
||||
|
||||
--BOUNDARY
|
||||
|
||||
+7
-10
@@ -294,11 +294,7 @@ impl MimeMessage {
|
||||
&mut wants_mdn,
|
||||
&mail,
|
||||
);
|
||||
headers_removed.extend(
|
||||
headers
|
||||
.extract_if(|k, _v| is_hidden(k))
|
||||
.map(|(k, _v)| k.to_string()),
|
||||
);
|
||||
headers_removed.extend(headers.extract_if(|k, _v| is_hidden(k)).map(|(k, _v)| k));
|
||||
|
||||
// Parse hidden headers.
|
||||
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
|
||||
@@ -935,9 +931,10 @@ impl MimeMessage {
|
||||
// See if an MDN is requested from the other side
|
||||
if self.decryption_error.is_none()
|
||||
&& (!self.parts.is_empty() || matches!(&self.pre_message, PreMessageMode::Pre { .. }))
|
||||
&& self.wants_mdn
|
||||
&& self.incoming
|
||||
&& let Some(part) = self.parts.last_mut()
|
||||
&& self.wants_mdn
|
||||
&& self.was_encrypted() // Do not send MDNs for unencrypted messages.
|
||||
&& self.incoming
|
||||
&& let Some(part) = self.parts.last_mut()
|
||||
{
|
||||
part.param.set_int(Param::WantsMdn, 1);
|
||||
}
|
||||
@@ -951,7 +948,7 @@ impl MimeMessage {
|
||||
typ: Viewtype::Text,
|
||||
..Default::default()
|
||||
};
|
||||
if self.wants_mdn && self.incoming {
|
||||
if self.wants_mdn && self.was_encrypted() && self.incoming {
|
||||
part.param.set_int(Param::WantsMdn, 1);
|
||||
}
|
||||
if let Some(ref subject) = self.get_subject()
|
||||
@@ -1751,7 +1748,7 @@ impl MimeMessage {
|
||||
headers_removed.extend(
|
||||
headers
|
||||
.extract_if(|k, _v| has_header_protection || is_protected(k))
|
||||
.map(|(k, _v)| k.to_string()),
|
||||
.map(|(k, _v)| k),
|
||||
);
|
||||
|
||||
if has_header_protection {
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ use tls::wrap_tls;
|
||||
pub(crate) const TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
/// TTL for caches in seconds.
|
||||
pub(crate) const CACHE_TTL: u32 = 30 * 24 * 60 * 60;
|
||||
pub(crate) const CACHE_TTL: u64 = 30 * 24 * 60 * 60;
|
||||
|
||||
/// Removes connection history entries after `CACHE_TTL`.
|
||||
pub(crate) async fn prune_connection_history(context: &Context) -> Result<()> {
|
||||
|
||||
+4
-2
@@ -410,7 +410,8 @@ impl ProxyConfig {
|
||||
};
|
||||
let tcp_stream =
|
||||
crate::net::connect_tcp(context, &hostname, http_config.port, load_cache)
|
||||
.await?;
|
||||
.await
|
||||
.context("Failed to connect to HTTP proxy")?;
|
||||
let auth = if let Some((username, password)) = &http_config.user_password {
|
||||
Some((username.as_str(), password.as_str()))
|
||||
} else {
|
||||
@@ -429,7 +430,8 @@ impl ProxyConfig {
|
||||
|
||||
let tcp_stream =
|
||||
crate::net::connect_tcp(context, &hostname, https_config.port, load_cache)
|
||||
.await?;
|
||||
.await
|
||||
.context("Failed to connect to HTTPS proxy")?;
|
||||
let use_sni = true;
|
||||
let tls_stream = wrap_rustls(
|
||||
&hostname,
|
||||
|
||||
@@ -46,7 +46,7 @@ use crate::mimeparser::SystemMessage;
|
||||
|
||||
/// The length of an ed25519 `PublicKey`, in bytes.
|
||||
const PUBLIC_KEY_LENGTH: usize = 32;
|
||||
const PUBLIC_KEY_STUB: &[u8] = "static_string".as_bytes();
|
||||
const PUBLIC_KEY_STUB: &[u8] = b"static_string";
|
||||
|
||||
/// Store Iroh peer channels for the context.
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -86,14 +86,14 @@ async fn test_can_communicate() {
|
||||
.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.send_webxdc_realtime_data(alice, alice_webxdc.id, "alice -> bob".as_bytes().to_vec())
|
||||
.send_webxdc_realtime_data(alice, alice_webxdc.id, b"alice -> bob".to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
let event = bob.evtracker.recv().await.unwrap();
|
||||
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
|
||||
if data == "alice -> bob".as_bytes() {
|
||||
if data == b"alice -> bob" {
|
||||
break;
|
||||
} else {
|
||||
panic!(
|
||||
@@ -107,14 +107,14 @@ async fn test_can_communicate() {
|
||||
bob.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice".as_bytes().to_vec())
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, b"bob -> alice".to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
let event = alice.evtracker.recv().await.unwrap();
|
||||
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
|
||||
if data == "bob -> alice".as_bytes() {
|
||||
if data == b"bob -> alice" {
|
||||
break;
|
||||
} else {
|
||||
panic!(
|
||||
@@ -149,14 +149,14 @@ async fn test_can_communicate() {
|
||||
bob.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice 2".as_bytes().to_vec())
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, b"bob -> alice 2".to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
let event = alice.evtracker.recv().await.unwrap();
|
||||
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
|
||||
if data == "bob -> alice 2".as_bytes() {
|
||||
if data == b"bob -> alice 2" {
|
||||
break;
|
||||
} else {
|
||||
panic!(
|
||||
@@ -314,14 +314,14 @@ async fn test_can_reconnect() {
|
||||
.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.send_webxdc_realtime_data(alice, alice_webxdc.id, "alice -> bob".as_bytes().to_vec())
|
||||
.send_webxdc_realtime_data(alice, alice_webxdc.id, b"alice -> bob".to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
let event = bob.evtracker.recv().await.unwrap();
|
||||
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
|
||||
if data == "alice -> bob".as_bytes() {
|
||||
if data == b"alice -> bob" {
|
||||
break;
|
||||
} else {
|
||||
panic!(
|
||||
@@ -373,14 +373,14 @@ async fn test_can_reconnect() {
|
||||
bob.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice".as_bytes().to_vec())
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, b"bob -> alice".to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
let event = alice.evtracker.recv().await.unwrap();
|
||||
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
|
||||
if data == "bob -> alice".as_bytes() {
|
||||
if data == b"bob -> alice" {
|
||||
break;
|
||||
} else {
|
||||
panic!(
|
||||
|
||||
+1
-1
@@ -401,7 +401,7 @@ pub fn merge_openpgp_certificates(
|
||||
// such as Alice's key in `test-data/key/alice-secret.asc`.
|
||||
let best_user: Option<SignedUser> = old_users
|
||||
.into_iter()
|
||||
.chain(new_users.clone())
|
||||
.chain(new_users)
|
||||
.filter_map(|SignedUser { id, signatures }| {
|
||||
// Select the best signature for each User ID.
|
||||
// If User ID has no valid signatures, it is filtered out.
|
||||
|
||||
@@ -384,7 +384,7 @@ fn test_merge_openpgp_certificates() {
|
||||
|
||||
// Cannot merge certificates with different primary key.
|
||||
assert!(merge_openpgp_certificates(alice.clone(), bob.clone()).is_err());
|
||||
assert!(merge_openpgp_certificates(bob.clone(), alice.clone()).is_err());
|
||||
assert!(merge_openpgp_certificates(bob, alice).is_err());
|
||||
}
|
||||
|
||||
/// Test PQC support.
|
||||
|
||||
+5
-9
@@ -1,6 +1,6 @@
|
||||
//! Handle plain text together with some attributes.
|
||||
|
||||
use std::sync::LazyLock;
|
||||
use regex::regex;
|
||||
|
||||
use crate::simplify::remove_message_footer;
|
||||
|
||||
@@ -25,12 +25,8 @@ impl PlainText {
|
||||
/// Convert plain text to HTML.
|
||||
/// The function handles quotes, links, fixed and floating text paragraphs.
|
||||
pub fn to_html(&self) -> String {
|
||||
static LINKIFY_MAIL_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"\b([\w.\-+]+@[\w.\-]+)\b").unwrap());
|
||||
|
||||
static LINKIFY_URL_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
|
||||
regex::Regex::new(r"\b((http|https|ftp|ftps):[\w.,:;$/@!?&%\-~=#+]+)").unwrap()
|
||||
});
|
||||
let linkify_mail_re = regex!(r"\b([\w.\-+]+@[\w.\-]+)\b");
|
||||
let linkify_url_re = regex!(r"\b((http|https|ftp|ftps):[\w.,:;$/@!?&%\-~=#+]+)");
|
||||
|
||||
let lines: Vec<&str> = self.text.lines().collect();
|
||||
let (lines, _footer) = remove_message_footer(&lines);
|
||||
@@ -52,12 +48,12 @@ impl PlainText {
|
||||
// to avoid double encoding, we escape our html-entities by \r that must not be used in the string elsewhere.
|
||||
let line = line.to_string().replace('\r', "");
|
||||
|
||||
let mut line = LINKIFY_MAIL_RE
|
||||
let mut line = linkify_mail_re
|
||||
.replace_all(&line, "\rLTa href=\rQUOTmailto:$1\rQUOT\rGT$1\rLT/a\rGT")
|
||||
.as_ref()
|
||||
.to_string();
|
||||
|
||||
line = LINKIFY_URL_RE
|
||||
line = linkify_url_re
|
||||
.replace_all(&line, "\rLTa href=\rQUOT$1\rQUOT\rGT$1\rLT/a\rGT")
|
||||
.as_ref()
|
||||
.to_string();
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
mod dclogin_scheme;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow, bail, ensure};
|
||||
pub use dclogin_scheme::LoginOptions;
|
||||
pub(crate) use dclogin_scheme::login_param_from_login_qr;
|
||||
use deltachat_contact_tools::{ContactAddress, addr_normalize, may_be_valid_addr};
|
||||
use percent_encoding::{NON_ALPHANUMERIC, percent_decode_str, percent_encode};
|
||||
use regex::regex;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::autorelay::login_param_from_host;
|
||||
@@ -764,7 +764,7 @@ fn decode_tg_socks_proxy(_context: &Context, qr: &str) -> Result<Qr> {
|
||||
fn decode_shadowsocks_proxy(qr: &str) -> Result<Qr> {
|
||||
let server_config = shadowsocks::config::ServerConfig::from_url(qr)?;
|
||||
let addr = server_config.addr();
|
||||
let host = addr.host().to_string();
|
||||
let host = addr.host();
|
||||
let port = addr.port();
|
||||
Ok(Qr::Proxy {
|
||||
url: qr.to_string(),
|
||||
@@ -1071,16 +1071,11 @@ async fn decode_matmsg(context: &Context, qr: &str) -> Result<Qr> {
|
||||
Qr::from_address(context, name, &addr, None).await
|
||||
}
|
||||
|
||||
static VCARD_NAME_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"(?m)^N:([^;]*);([^;\n]*)").unwrap());
|
||||
static VCARD_EMAIL_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"(?m)^EMAIL([^:\n]*):([^;\n]*)").unwrap());
|
||||
|
||||
/// Extract address for the vcard scheme.
|
||||
///
|
||||
/// Scheme: `VCARD:BEGIN\nN:last name;first name;...;\nEMAIL;<type>:addr...;`
|
||||
async fn decode_vcard(context: &Context, qr: &str) -> Result<Qr> {
|
||||
let name = VCARD_NAME_RE
|
||||
let name = regex!(r"(?m)^N:([^;]*);([^;\n]*)")
|
||||
.captures(qr)
|
||||
.and_then(|caps| {
|
||||
let last_name = caps.get(1)?.as_str().trim();
|
||||
@@ -1090,7 +1085,10 @@ async fn decode_vcard(context: &Context, qr: &str) -> Result<Qr> {
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let addr = if let Some(cap) = VCARD_EMAIL_RE.captures(qr).and_then(|caps| caps.get(2)) {
|
||||
let addr = if let Some(cap) = regex!(r"(?m)^EMAIL([^:\n]*):([^;\n]*)")
|
||||
.captures(qr)
|
||||
.and_then(|caps| caps.get(2))
|
||||
{
|
||||
normalize_address(cap.as_str().trim())?
|
||||
} else {
|
||||
bail!("Bad e-mail address");
|
||||
@@ -1124,7 +1122,7 @@ fn normalize_address(addr: &str) -> Result<String> {
|
||||
|
||||
ensure!(may_be_valid_addr(&new_addr), "Bad e-mail address");
|
||||
|
||||
Ok(new_addr.to_string())
|
||||
Ok(new_addr)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -120,7 +120,7 @@ pub(super) fn decode_login(qr: &str) -> Result<Qr> {
|
||||
};
|
||||
|
||||
Ok(Qr::Login {
|
||||
address: addr.to_owned(),
|
||||
address: addr,
|
||||
options,
|
||||
})
|
||||
} else {
|
||||
|
||||
+3
-3
@@ -84,7 +84,7 @@ pub struct ReactionFrequency {
|
||||
pub reaction: Reaction,
|
||||
|
||||
/// Number of contacts that reacted with this emoji.
|
||||
pub count: u32,
|
||||
pub count: usize,
|
||||
|
||||
/// True if `ContactId::SELF` is among the contacts that reacted with this emoji.
|
||||
pub is_from_self: bool,
|
||||
@@ -165,7 +165,7 @@ async fn set_msg_id_reaction(
|
||||
.await?;
|
||||
if chat
|
||||
.param
|
||||
.update_timestamp(Param::LastReactionTimestamp, timestamp)?
|
||||
.update_timestamp(Param::LastReactionTimestamp, timestamp)
|
||||
{
|
||||
chat.param
|
||||
.set_i64(Param::LastReactionMsgId, i64::from(msg_id.to_u32()));
|
||||
@@ -420,7 +420,7 @@ pub(crate) async fn apply_pending_reactions(
|
||||
/// sorted in descending order of frequency.
|
||||
fn calc_frequencies(by_contact: &BTreeMap<ContactId, Reaction>) -> Vec<ReactionFrequency> {
|
||||
let mut self_reaction = Reaction::new("");
|
||||
let mut counts: BTreeMap<&str, u32> = BTreeMap::new();
|
||||
let mut counts: BTreeMap<&str, usize> = BTreeMap::new();
|
||||
for (contact_id, reaction) in by_contact {
|
||||
let count = counts.entry(reaction.as_str()).or_insert(0);
|
||||
*count = count.saturating_add(1);
|
||||
|
||||
@@ -42,7 +42,7 @@ struct WireMessage {
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct WireEntry {
|
||||
emoji: String,
|
||||
count: u32,
|
||||
count: usize,
|
||||
}
|
||||
|
||||
/// Renders one or more message's states as a JSON string, ready to be sent in `Chat-Broadcast-States:` header.
|
||||
@@ -235,10 +235,10 @@ pub(crate) async fn load_broadcast_reactions(
|
||||
(msg_id,),
|
||||
|row| {
|
||||
let reaction: String = row.get(0)?;
|
||||
let count: u32 = row.get(1)?;
|
||||
let count: i64 = row.get(1)?;
|
||||
Ok(ReactionFrequency {
|
||||
reaction: Reaction::new(&reaction),
|
||||
count,
|
||||
count: count as usize,
|
||||
is_from_self: false,
|
||||
})
|
||||
},
|
||||
@@ -405,7 +405,7 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_modify_frequencies() {
|
||||
// Helper to create a ReactionFrequency entry
|
||||
let freq = |emoji: &str, count: u32, is_from_self: bool| -> ReactionFrequency {
|
||||
let freq = |emoji: &str, count: usize, is_from_self: bool| -> ReactionFrequency {
|
||||
ReactionFrequency {
|
||||
reaction: Reaction::new(emoji),
|
||||
count,
|
||||
|
||||
+8
-13
@@ -483,7 +483,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
}
|
||||
|
||||
let trash = || async {
|
||||
let msg_ids = vec![insert_tombstone(context, rfc724_mid).await?];
|
||||
let msg_ids = vec![insert_tombstone(context, rfc724_mid, false).await?];
|
||||
Ok(Some(ReceivedMsg {
|
||||
chat_id: ChatId::TRASH,
|
||||
state: MessageState::Undefined,
|
||||
@@ -668,14 +668,15 @@ pub(crate) async fn receive_imf_inner(
|
||||
|
||||
match res {
|
||||
securejoin::HandshakeMessage::Done | securejoin::HandshakeMessage::Ignore => {
|
||||
let msg_id = insert_tombstone(context, rfc724_mid).await?;
|
||||
let needs_delete_job = res == securejoin::HandshakeMessage::Done;
|
||||
let msg_id = insert_tombstone(context, rfc724_mid, needs_delete_job).await?;
|
||||
received_msg = Some(ReceivedMsg {
|
||||
chat_id: ChatId::TRASH,
|
||||
state: MessageState::InSeen,
|
||||
hidden: false,
|
||||
sort_timestamp: mime_parser.timestamp_sent,
|
||||
msg_ids: vec![msg_id],
|
||||
needs_delete_job: res == securejoin::HandshakeMessage::Done,
|
||||
needs_delete_job,
|
||||
});
|
||||
}
|
||||
securejoin::HandshakeMessage::Propagate => {
|
||||
@@ -2311,7 +2312,7 @@ INSERT INTO msgs
|
||||
// This way, `LastSubject` actually refers to the most recent message _shown_ in the chat.
|
||||
if chat
|
||||
.param
|
||||
.update_timestamp(Param::SubjectTimestamp, sort_timestamp)?
|
||||
.update_timestamp(Param::SubjectTimestamp, sort_timestamp)
|
||||
{
|
||||
// write the last subject even if empty -
|
||||
// otherwise a reply may get an outdated subject.
|
||||
@@ -2401,7 +2402,7 @@ async fn handle_edit_delete(
|
||||
let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? else {
|
||||
warn!(context, "Delete message: {rfc724_mid:?} not found.");
|
||||
// Insert a tombstone so that the message will be ignored if it arrives later within a period specified in prune_tombstones().
|
||||
insert_tombstone(context, rfc724_mid).await?;
|
||||
insert_tombstone(context, rfc724_mid, false).await?;
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -2716,12 +2717,6 @@ async fn lookup_or_create_adhoc_group(
|
||||
for &id in &contact_ids {
|
||||
stmt.execute((id,)).context("INSERT INTO temp.contacts")?;
|
||||
}
|
||||
|
||||
// Contact IDs are 32-bit internally,
|
||||
// so this conversion of contact ID set size
|
||||
// to u32 should never fail.
|
||||
let contact_ids_len = u32::try_from(contact_ids.len())?;
|
||||
|
||||
let val = t
|
||||
.query_row(
|
||||
"SELECT c.id, c.blocked
|
||||
@@ -2735,7 +2730,7 @@ async fn lookup_or_create_adhoc_group(
|
||||
AND contact_id NOT IN (SELECT id FROM temp.contacts)
|
||||
AND add_timestamp >= remove_timestamp)=0
|
||||
ORDER BY m.timestamp DESC",
|
||||
(&grpname, contact_ids_len),
|
||||
(&grpname, contact_ids.len()),
|
||||
|row| {
|
||||
let id: ChatId = row.get(0)?;
|
||||
let blocked: Blocked = row.get(1)?;
|
||||
@@ -3420,7 +3415,7 @@ async fn apply_chat_name_avatar_and_description_changes(
|
||||
&& is_from_in_chat
|
||||
&& chat
|
||||
.param
|
||||
.update_timestamp(Param::AvatarTimestamp, mime_parser.timestamp_sent)?
|
||||
.update_timestamp(Param::AvatarTimestamp, mime_parser.timestamp_sent)
|
||||
{
|
||||
info!(context, "Group-avatar change for {}.", chat.id);
|
||||
match avatar_action {
|
||||
|
||||
@@ -416,7 +416,9 @@ async fn test_escaped_from() {
|
||||
);
|
||||
let msg = get_chat_msg(&t, chat_id, 0, 1).await;
|
||||
assert_eq!(msg.text, "hello");
|
||||
assert_eq!(msg.param.get_int(Param::WantsMdn).unwrap(), 1);
|
||||
|
||||
// MDN request from unencrypted message is ignored.
|
||||
assert!(msg.param.get_int(Param::WantsMdn).is_none());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
+16
-21
@@ -33,14 +33,14 @@ pub(crate) use qrinvite::QrInvite;
|
||||
|
||||
use crate::token::Namespace;
|
||||
|
||||
const DISALLOWED_CHARACTERS: &AsciiSet = &NON_ALPHANUMERIC_WITHOUT_DOT.remove(b'_');
|
||||
const DISALLOWED_CHARACTERS: &AsciiSet = &NON_ALPHANUMERIC_WITHOUT_DOT.remove(b'_').remove(b'@');
|
||||
|
||||
fn inviter_progress(
|
||||
context: &Context,
|
||||
contact_id: ContactId,
|
||||
chat_id: ChatId,
|
||||
chat_type: Chattype,
|
||||
) -> Result<()> {
|
||||
) {
|
||||
// No other values are used.
|
||||
let progress = 1000;
|
||||
context.emit_event(EventType::SecurejoinInviterProgress {
|
||||
@@ -49,8 +49,6 @@ fn inviter_progress(
|
||||
chat_type,
|
||||
progress,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shorten name to max. `length` characters.
|
||||
@@ -121,21 +119,18 @@ pub async fn get_securejoin_qr(context: &Context, chat: Option<ChatId>) -> Resul
|
||||
|
||||
let fingerprint = self_fingerprint(context).await?;
|
||||
|
||||
let self_addr = context.get_primary_self_addr().await?;
|
||||
let self_addr_urlencoded = utf8_percent_encode(&self_addr, DISALLOWED_CHARACTERS).to_string();
|
||||
let self_addrs = context.get_self_addrs().await?;
|
||||
let mut encoded_addrs = self_addrs
|
||||
.iter()
|
||||
.map(|addr| utf8_percent_encode(addr, DISALLOWED_CHARACTERS).to_string());
|
||||
let self_addr_urlencoded = encoded_addrs.next().context("No self addr configured")?;
|
||||
let encoded_extra_relays: Vec<String> = encoded_addrs.collect();
|
||||
|
||||
let r_param = context
|
||||
.get_self_addrs()
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|addr| *addr != self_addr)
|
||||
.reduce(|acc, addr| {
|
||||
format!(
|
||||
"{acc},{}",
|
||||
utf8_percent_encode(&addr, DISALLOWED_CHARACTERS)
|
||||
)
|
||||
})
|
||||
.map_or(String::default(), |addrs| format!("&r={addrs}"));
|
||||
let r_param = if encoded_extra_relays.is_empty() {
|
||||
"".to_string()
|
||||
} else {
|
||||
format!("&r={}", encoded_extra_relays.join(","))
|
||||
};
|
||||
|
||||
let self_name = context
|
||||
.get_config(Config::Displayname)
|
||||
@@ -659,7 +654,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
|
||||
}
|
||||
|
||||
inviter_progress(context, contact_id, joining_chat_id, chat.typ)?;
|
||||
inviter_progress(context, contact_id, joining_chat_id, chat.typ);
|
||||
// IMAP-delete the message to avoid handling it by another device and adding the
|
||||
// member twice. Another device will know the member's key from Autocrypt-Gossip.
|
||||
Ok(HandshakeMessage::Done)
|
||||
@@ -670,7 +665,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
.await
|
||||
.context("failed sending vc-contact-confirm message")?;
|
||||
|
||||
inviter_progress(context, contact_id, chat_id, Chattype::Single)?;
|
||||
inviter_progress(context, contact_id, chat_id, Chattype::Single);
|
||||
Ok(HandshakeMessage::Ignore) // "Done" would delete the message and break multi-device (the key from Autocrypt-header is needed)
|
||||
}
|
||||
}
|
||||
@@ -817,7 +812,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
// and tests which don't care about the chat ID,
|
||||
// so we pass invalid chat ID here.
|
||||
let chat_id = ChatId::new(0);
|
||||
inviter_progress(context, contact_id, chat_id, chat_type)?;
|
||||
inviter_progress(context, contact_id, chat_id, chat_type);
|
||||
}
|
||||
|
||||
if matches!(step, SecureJoinStep::MemberAdded) {
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::chat::{CantSendReason, ChatId, add_contact_to_chat, remove_contact_fr
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::message::rfc724_mid_exists_ext;
|
||||
use crate::qr::Qr;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::stock_str::{self, messages_e2ee_info_msg};
|
||||
@@ -13,6 +14,7 @@ use crate::test_utils::{
|
||||
AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext, TestContextManager,
|
||||
TimeShiftFalsePositiveNote, get_chat_msg, sync,
|
||||
};
|
||||
use crate::transport::add_pseudo_transport;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum SetupContactCase {
|
||||
@@ -1142,6 +1144,43 @@ async fn test_get_securejoin_qr_name_is_last() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that addresses in QR codes are percent-encoded.
|
||||
/// `@` should not be encoded unnecessarily,
|
||||
/// since this would just make the QR code longer.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_securejoin_qr_encoding() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
// `@` in email addresses must not be percent-encoded:
|
||||
add_pseudo_transport(alice, "asdf@example.org").await?;
|
||||
// But `%` does need percent-encoding:
|
||||
add_pseudo_transport(alice, "jk%l@example.net").await?;
|
||||
|
||||
let qr = get_securejoin_qr(alice, None).await?;
|
||||
assert!(
|
||||
qr.contains("a=jk%25l@example.net"),
|
||||
"{qr} doesn't contain 'a=jk%25l@example.net'"
|
||||
);
|
||||
assert!(
|
||||
qr.contains("r=asdf@example.org,alice@example.org"),
|
||||
"{qr} doesn't contain 'r=asdf@example.org,alice@example.org'"
|
||||
);
|
||||
|
||||
let qr = check_qr(bob, &qr).await?;
|
||||
let Qr::AskVerifyContact { mut addrs, .. } = qr else {
|
||||
unreachable!()
|
||||
};
|
||||
addrs.sort();
|
||||
assert_eq!(
|
||||
addrs,
|
||||
vec!["alice@example.org", "asdf@example.org", "jk%l@example.net",]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// QR codes should not get arbitrary big because of long names.
|
||||
/// The truncation, however, should not let the url end with a `.`, which is a call for trouble in linkfiers.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -1525,3 +1564,29 @@ async fn test_deduplicate_member_added() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that a handled join request is also marked as "deleted"
|
||||
/// in the database, so that if a copy of the request arrives via
|
||||
/// other relays in the future, then this copy will also be deleted.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_join_request_deleted_on_all_relays() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_chat_id = chat::create_group(alice, "Group").await?;
|
||||
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await?;
|
||||
bob.add_or_lookup_contact_id(alice).await;
|
||||
join_securejoin(bob, &qr).await?;
|
||||
let request = bob.pop_sent_msg().await;
|
||||
alice.recv_msg_trash(&request).await;
|
||||
|
||||
let rfc724_mid = Message::load_from_db(bob, request.sender_msg_id)
|
||||
.await?
|
||||
.rfc724_mid;
|
||||
let (_, deleted) = rfc724_mid_exists_ext(alice, &rfc724_mid, "deleted=1")
|
||||
.await?
|
||||
.unwrap();
|
||||
assert!(deleted);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+35
-25
@@ -22,7 +22,6 @@ use crate::log::{LogExt, warn};
|
||||
use crate::message::Message;
|
||||
use crate::message::{self, MsgId};
|
||||
use crate::mimefactory;
|
||||
use crate::mimefactory::MimeFactory;
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::net::session::SessionBufStream;
|
||||
use crate::scheduler::connectivity::ConnectivityStore;
|
||||
@@ -69,6 +68,8 @@ impl Smtp {
|
||||
// separate task to avoid waiting for reply or timeout.
|
||||
task::spawn(async move { transport.quit().await });
|
||||
}
|
||||
self.transport_id = None;
|
||||
self.from = None;
|
||||
self.last_success = None;
|
||||
}
|
||||
|
||||
@@ -356,7 +357,7 @@ pub(crate) async fn insert_into_smtp(
|
||||
queued_msg: &QueuedMail,
|
||||
) -> Result<()> {
|
||||
let now = tools::time();
|
||||
let msg_id = message::insert_tombstone(context, rfc724_mid).await?;
|
||||
let msg_id = message::insert_tombstone(context, rfc724_mid, false).await?;
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| queue::enqueue_mail(transaction, now, msg_id, queued_msg, None))
|
||||
@@ -652,6 +653,24 @@ pub(crate) async fn send_smtp_messages(context: &Context, connection: &mut Smtp)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_mdns_by_rfc724_mid(
|
||||
context: &Context,
|
||||
rfc724_mid: &str,
|
||||
additional_rfc724_mids: Vec<String>,
|
||||
) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let mut stmt = transaction.prepare("DELETE FROM smtp_mdns WHERE rfc724_mid = ?")?;
|
||||
stmt.execute((rfc724_mid,))?;
|
||||
for additional_rfc724_mid in additional_rfc724_mids {
|
||||
stmt.execute((additional_rfc724_mid,))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Tries to send MDN for message identified by `rfc724_mdn` to `contact_id`.
|
||||
///
|
||||
/// Attempts to aggregate additional MDNs for `contact_id` into sent MDN.
|
||||
@@ -687,28 +706,30 @@ async fn send_mdn_rfc724_mid(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mimefactory = MimeFactory::from_mdn(
|
||||
let queued_mdn = mimefactory::mdn(
|
||||
context,
|
||||
contact_id,
|
||||
rfc724_mid.to_string(),
|
||||
rfc724_mid,
|
||||
additional_rfc724_mids.clone(),
|
||||
)
|
||||
.await?;
|
||||
let encrypted = mimefactory.will_be_encrypted();
|
||||
let mut recipients = if contact_id == ContactId::SELF {
|
||||
Vec::new()
|
||||
} else {
|
||||
mimefactory.recipients()
|
||||
};
|
||||
let bcc_self = queued_mdn.bcc_self;
|
||||
|
||||
let encrypted = queued_mdn.encryption.is_encrypted();
|
||||
let mut recipients = queued_mdn.recipients.clone();
|
||||
|
||||
let public_key = key::load_self_public_key(context).await?;
|
||||
let secret_key = key::load_self_secret_key(context).await?;
|
||||
let from = smtp
|
||||
.from
|
||||
.as_ref()
|
||||
.context("No From address, not connected")?
|
||||
.to_string();
|
||||
let rendered_msg = Box::pin(mimefactory.render(context, &from)).await?;
|
||||
let rendered_msg =
|
||||
mimefactory::render_queued_mail(queued_mdn, &public_key, &secret_key, from.clone())?;
|
||||
let body = rendered_msg.message;
|
||||
|
||||
if context.get_config_bool(Config::BccSelf).await? {
|
||||
if bcc_self {
|
||||
add_self_recipients(context, &mut recipients, encrypted, from).await?;
|
||||
}
|
||||
let recipients: Vec<_> = recipients
|
||||
@@ -720,24 +741,13 @@ async fn send_mdn_rfc724_mid(
|
||||
.ok()
|
||||
})
|
||||
.collect();
|
||||
message::insert_tombstone(context, &rendered_msg.rfc724_mid).await?;
|
||||
message::insert_tombstone(context, &rendered_msg.rfc724_mid, false).await?;
|
||||
match smtp_send(context, &recipients, &body, smtp, None).await {
|
||||
SendResult::Success => {
|
||||
if !recipients.is_empty() {
|
||||
info!(context, "Successfully sent MDN for {rfc724_mid}.");
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let mut stmt =
|
||||
transaction.prepare("DELETE FROM smtp_mdns WHERE rfc724_mid = ?")?;
|
||||
stmt.execute((rfc724_mid,))?;
|
||||
for additional_rfc724_mid in additional_rfc724_mids {
|
||||
stmt.execute((additional_rfc724_mid,))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
delete_mdns_by_rfc724_mid(context, rfc724_mid, additional_rfc724_mids).await?;
|
||||
Ok(true)
|
||||
}
|
||||
SendResult::Retry => {
|
||||
|
||||
+1
-1
@@ -109,7 +109,7 @@ pub struct SideEffects {
|
||||
}
|
||||
|
||||
/// Email message ready to be queued with the side effects that should be applied at the same time.
|
||||
pub(crate) type ToBeQueuedMail = (QueuedMail, Option<SideEffects>);
|
||||
pub(crate) type ToBeQueuedMail = (QueuedMail, SideEffects);
|
||||
|
||||
/// Process side effects and store queued mail.
|
||||
pub(crate) fn enqueue_mail(
|
||||
|
||||
+120
-16
@@ -14,7 +14,7 @@ use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::debug_logging::set_debug_logging_xdc;
|
||||
use crate::ephemeral::start_ephemeral_timers;
|
||||
use crate::imex::BLOBS_BACKUP_NAME;
|
||||
use crate::imex::{self, BLOBS_BACKUP_NAME};
|
||||
use crate::location;
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::message::MsgId;
|
||||
@@ -56,6 +56,10 @@ pub struct Sql {
|
||||
/// SQL connection pool.
|
||||
pool: RwLock<Option<Pool>>,
|
||||
|
||||
/// None if the database is not open, true if it is open with passphrase and false if it is
|
||||
/// open without a passphrase.
|
||||
is_encrypted: RwLock<Option<bool>>,
|
||||
|
||||
/// Cache of `config` table.
|
||||
pub(crate) config_cache: RwLock<HashMap<String, Option<String>>>,
|
||||
}
|
||||
@@ -66,15 +70,52 @@ impl Sql {
|
||||
Self {
|
||||
dbfile,
|
||||
pool: Default::default(),
|
||||
is_encrypted: Default::default(),
|
||||
config_cache: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests SQLCipher passphrase.
|
||||
///
|
||||
/// Returns true if passphrase is correct, i.e. the database is new or can be unlocked with
|
||||
/// this passphrase, and false if the database is already encrypted with another passphrase or
|
||||
/// corrupted.
|
||||
///
|
||||
/// Fails if database is already open.
|
||||
pub async fn check_passphrase(&self, passphrase: String) -> Result<bool> {
|
||||
if self.is_open().await {
|
||||
bail!("Database is already opened.");
|
||||
}
|
||||
|
||||
// Hold the lock to prevent other thread from opening the database.
|
||||
let _lock = self.pool.write().await;
|
||||
|
||||
// Test that the key is correct using a single connection.
|
||||
let connection = Connection::open(&self.dbfile)?;
|
||||
if !passphrase.is_empty() {
|
||||
connection
|
||||
.pragma_update(None, "key", &passphrase)
|
||||
.context("Failed to set PRAGMA key")?;
|
||||
}
|
||||
let key_is_correct = connection
|
||||
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
|
||||
.is_ok();
|
||||
|
||||
Ok(key_is_correct)
|
||||
}
|
||||
|
||||
/// Checks if there is currently a connection to the underlying Sqlite database.
|
||||
pub async fn is_open(&self) -> bool {
|
||||
self.pool.read().await.is_some()
|
||||
}
|
||||
|
||||
/// Returns true if the database is encrypted.
|
||||
///
|
||||
/// If database is not open, returns `None`.
|
||||
pub(crate) async fn is_encrypted(&self) -> Option<bool> {
|
||||
*self.is_encrypted.read().await
|
||||
}
|
||||
|
||||
/// Closes all underlying Sqlite connections.
|
||||
pub(crate) async fn close(&self) {
|
||||
let _ = self.pool.write().await.take();
|
||||
@@ -82,22 +123,52 @@ impl Sql {
|
||||
}
|
||||
|
||||
/// Imports the database from a separate file with the given passphrase.
|
||||
pub(crate) async fn import(&self, path: &Path) -> Result<()> {
|
||||
pub(crate) async fn import(&self, path: &Path, passphrase: String) -> Result<()> {
|
||||
let path_str = path
|
||||
.to_str()
|
||||
.with_context(|| format!("path {path:?} is not valid unicode"))?
|
||||
.to_string();
|
||||
|
||||
// Keep `config_cache` locked all the time the db is imported so that nobody can use invalid
|
||||
// values from there. And clear it immediately so as not to forget in case of errors.
|
||||
let mut config_cache = self.config_cache.write().await;
|
||||
config_cache.clear();
|
||||
|
||||
let src_conn =
|
||||
rusqlite::Connection::open(path).context("Failed to open source database")?;
|
||||
let query_only = false;
|
||||
self.call(query_only, move |conn| {
|
||||
let backup = rusqlite::backup::Backup::new(&src_conn, &mut *conn)?;
|
||||
backup.run_to_completion(5, std::time::Duration::ZERO, None)?;
|
||||
drop(backup);
|
||||
// Check that backup passphrase is correct before resetting our database.
|
||||
conn.execute("ATTACH DATABASE ? AS backup KEY ?", (path_str, passphrase))
|
||||
.context("failed to attach backup database")?;
|
||||
let res = conn
|
||||
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
|
||||
.context("backup passphrase is not correct");
|
||||
|
||||
conn.execute("VACUUM", [])
|
||||
.context("failed to vacuum the database")?;
|
||||
// Reset the database without reopening it. We don't want to reopen the database because we
|
||||
// don't have main database passphrase at this point.
|
||||
// See <https://sqlite.org/c3ref/c_dbconfig_enable_fkey.html> for documentation.
|
||||
// Without resetting import may fail due to existing tables.
|
||||
res.and_then(|_| {
|
||||
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, true)
|
||||
.context("failed to set SQLITE_DBCONFIG_RESET_DATABASE")
|
||||
})
|
||||
.and_then(|_| {
|
||||
conn.execute("VACUUM", [])
|
||||
.context("failed to vacuum the database")
|
||||
})
|
||||
.and(
|
||||
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, false)
|
||||
.context("failed to unset SQLITE_DBCONFIG_RESET_DATABASE"),
|
||||
)
|
||||
.and_then(|_| {
|
||||
conn.query_row("SELECT sqlcipher_export('main', 'backup')", [], |_row| {
|
||||
Ok(())
|
||||
})
|
||||
.context("failed to import from attached backup database")
|
||||
})
|
||||
.and(
|
||||
conn.execute("DETACH DATABASE backup", [])
|
||||
.context("failed to detach backup database"),
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
@@ -106,10 +177,10 @@ impl Sql {
|
||||
const N_DB_CONNECTIONS: usize = 3;
|
||||
|
||||
/// Creates a new connection pool.
|
||||
fn new_pool(dbfile: &Path) -> Result<Pool> {
|
||||
fn new_pool(dbfile: &Path, passphrase: String) -> Result<Pool> {
|
||||
let mut connections = Vec::with_capacity(Self::N_DB_CONNECTIONS);
|
||||
for _ in 0..Self::N_DB_CONNECTIONS {
|
||||
let connection = new_connection(dbfile)?;
|
||||
let connection = new_connection(dbfile, &passphrase)?;
|
||||
connections.push(connection);
|
||||
}
|
||||
|
||||
@@ -117,8 +188,8 @@ impl Sql {
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
async fn try_open(&self, context: &Context, dbfile: &Path) -> Result<()> {
|
||||
*self.pool.write().await = Some(Self::new_pool(dbfile)?);
|
||||
async fn try_open(&self, context: &Context, dbfile: &Path, passphrase: String) -> Result<()> {
|
||||
*self.pool.write().await = Some(Self::new_pool(dbfile, passphrase.to_string())?);
|
||||
|
||||
if let Err(e) = self.run_migrations(context).await {
|
||||
error!(context, "Running migrations failed: {e:#}");
|
||||
@@ -176,7 +247,7 @@ impl Sql {
|
||||
|
||||
/// Opens the provided database and runs any necessary migrations.
|
||||
/// If a database is already open, this will return an error.
|
||||
pub async fn open(&self, context: &Context) -> Result<()> {
|
||||
pub async fn open(&self, context: &Context, passphrase: String) -> Result<()> {
|
||||
if self.is_open().await {
|
||||
error!(
|
||||
context,
|
||||
@@ -185,8 +256,10 @@ impl Sql {
|
||||
bail!("SQL database is already opened.");
|
||||
}
|
||||
|
||||
self.try_open(context, &self.dbfile).await?;
|
||||
let passphrase_nonempty = !passphrase.is_empty();
|
||||
self.try_open(context, &self.dbfile, passphrase).await?;
|
||||
info!(context, "Opened database {:?}.", self.dbfile);
|
||||
*self.is_encrypted.write().await = Some(passphrase_nonempty);
|
||||
|
||||
// setup debug logging if there is an entry containing its id
|
||||
if let Some(xdc_id) = self
|
||||
@@ -198,6 +271,28 @@ impl Sql {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Changes the passphrase of encrypted database.
|
||||
///
|
||||
/// The database must already be encrypted and the passphrase cannot be empty.
|
||||
/// It is impossible to turn encrypted database into unencrypted
|
||||
/// and vice versa this way, use import/export for this.
|
||||
pub async fn change_passphrase(&self, passphrase: String) -> Result<()> {
|
||||
let mut lock = self.pool.write().await;
|
||||
|
||||
let pool = lock.take().context("SQL connection pool is not open")?;
|
||||
let query_only = false;
|
||||
let conn = pool.get(query_only).await?;
|
||||
if !passphrase.is_empty() {
|
||||
conn.pragma_update(None, "rekey", passphrase.clone())
|
||||
.context("Failed to set PRAGMA rekey")?;
|
||||
}
|
||||
drop(pool);
|
||||
|
||||
*lock = Some(Self::new_pool(&self.dbfile, passphrase.to_string())?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Allocates a connection and calls `function` with the connection.
|
||||
///
|
||||
/// If `query_only` is true, allocates read-only connection,
|
||||
@@ -595,7 +690,7 @@ impl Sql {
|
||||
///
|
||||
/// `passphrase` is the SQLCipher database passphrase.
|
||||
/// Empty string if database is not encrypted.
|
||||
fn new_connection(path: &Path) -> Result<Connection> {
|
||||
fn new_connection(path: &Path, passphrase: &str) -> Result<Connection> {
|
||||
let flags = OpenFlags::SQLITE_OPEN_NO_MUTEX
|
||||
| OpenFlags::SQLITE_OPEN_READ_WRITE
|
||||
| OpenFlags::SQLITE_OPEN_CREATE;
|
||||
@@ -631,6 +726,9 @@ fn new_connection(path: &Path) -> Result<Connection> {
|
||||
conn.busy_timeout(Duration::ZERO)?;
|
||||
}
|
||||
|
||||
if !passphrase.is_empty() {
|
||||
conn.pragma_update(None, "key", passphrase)?;
|
||||
}
|
||||
// Try to enable auto_vacuum. This will only be
|
||||
// applied if the database is new or after successful
|
||||
// VACUUM, which usually happens before backup export.
|
||||
@@ -729,6 +827,12 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
|
||||
);
|
||||
}
|
||||
|
||||
imex::maybe_readd_backup_transfer_msg(context)
|
||||
.await
|
||||
.context("Failed to re-add backup transfer message")
|
||||
.log_err(context)
|
||||
.ok();
|
||||
|
||||
if let Err(err) = incremental_vacuum(context).await {
|
||||
warn!(context, "Failed to run incremental vacuum: {err:#}.");
|
||||
}
|
||||
|
||||
+7
-15
@@ -32,8 +32,6 @@ fn migrate_key_contacts(
|
||||
context: &Context,
|
||||
transaction: &mut rusqlite::Transaction<'_>,
|
||||
) -> std::result::Result<(), anyhow::Error> {
|
||||
info!(context, "Starting key-contact transition.");
|
||||
|
||||
// =============================== Step 1: ===============================
|
||||
// Alter tables
|
||||
transaction.execute_batch(
|
||||
@@ -79,13 +77,12 @@ fn migrate_key_contacts(
|
||||
.optional()
|
||||
.context("Step 0")?
|
||||
else {
|
||||
info!(
|
||||
context,
|
||||
"Not yet configured, no need to migrate key-contacts"
|
||||
);
|
||||
// Not yet configured, no need to migrate key-contacts.
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
info!(context, "Starting key-contact transition.");
|
||||
|
||||
// =============================== Step 2: ===============================
|
||||
// Create up to 3 new contacts for every contact that has a peerstate:
|
||||
// one from the Autocrypt key fingerprint, one from the verified key fingerprint,
|
||||
@@ -657,7 +654,7 @@ pub(crate) async fn msgs_to_key_contacts(context: &Context) -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
let trans_fn = |t: &mut rusqlite::Transaction| {
|
||||
let mut first_key_contacts_msg_id: u32 = t
|
||||
let mut first_key_contacts_msg_id: u64 = t
|
||||
.query_one(
|
||||
"SELECT CAST(value AS INTEGER) FROM config WHERE keyname='first_key_contacts_msg_id'",
|
||||
(),
|
||||
@@ -681,10 +678,10 @@ pub(crate) async fn msgs_to_key_contacts(context: &Context) -> Result<()> {
|
||||
)
|
||||
.context("Prepare stmt")?;
|
||||
let msgs_to_migrate = 1000;
|
||||
let mut msgs_migrated: u32 = 0;
|
||||
let mut msgs_migrated: u64 = 0;
|
||||
while first_key_contacts_msg_id > 0 && msgs_migrated < msgs_to_migrate {
|
||||
let start_msg_id = first_key_contacts_msg_id.saturating_sub(msgs_to_migrate);
|
||||
let cnt: u32 = stmt
|
||||
let cnt: u64 = stmt
|
||||
.execute((start_msg_id, first_key_contacts_msg_id))
|
||||
.context("UPDATE msgs")?
|
||||
.try_into()?;
|
||||
@@ -1936,14 +1933,9 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint);
|
||||
|
||||
inc_and_check(&mut migration_version, 132)?;
|
||||
if dbversion < migration_version {
|
||||
let start = Time::now();
|
||||
sql.execute_migration_transaction(|t| migrate_key_contacts(context, t), migration_version)
|
||||
.await?;
|
||||
info!(
|
||||
context,
|
||||
"key-contacts migration took {:?} in total.",
|
||||
time_elapsed(&start),
|
||||
);
|
||||
|
||||
// Schedule `msgs_to_key_contacts()`.
|
||||
context
|
||||
.set_config_internal(Config::LastHousekeeping, None)
|
||||
|
||||
+96
-3
@@ -83,7 +83,7 @@ async fn test_housekeeping_db_closed() {
|
||||
|
||||
t.sql.close().await;
|
||||
housekeeping(&t).await.unwrap(); // housekeeping should emit warnings but not fail
|
||||
t.sql.open(&t).await.unwrap();
|
||||
t.sql.open(&t, "".to_string()).await.unwrap();
|
||||
|
||||
let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||
assert_eq!(avatar_bytes, &tokio::fs::read(&a).await.unwrap()[..]);
|
||||
@@ -155,11 +155,11 @@ async fn test_db_reopen() -> Result<()> {
|
||||
let sql = Sql::new(dbfile);
|
||||
|
||||
// Create database with all the tables.
|
||||
sql.open(&t).await.unwrap();
|
||||
sql.open(&t, "".to_string()).await.unwrap();
|
||||
sql.close().await;
|
||||
|
||||
// Reopen the database
|
||||
sql.open(&t).await?;
|
||||
sql.open(&t, "".to_string()).await?;
|
||||
sql.execute(
|
||||
"INSERT INTO config (keyname, value) VALUES (?, ?);",
|
||||
("foo", "bar"),
|
||||
@@ -209,6 +209,99 @@ async fn test_migration_flags() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_check_passphrase() -> Result<()> {
|
||||
use tempfile::tempdir;
|
||||
|
||||
// The context is used only for logging.
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// Create a separate empty database for testing.
|
||||
let dir = tempdir()?;
|
||||
let dbfile = dir.path().join("testdb.sqlite");
|
||||
let sql = Sql::new(dbfile.clone());
|
||||
|
||||
sql.check_passphrase("foo".to_string()).await?;
|
||||
sql.open(&t, "foo".to_string())
|
||||
.await
|
||||
.context("failed to open the database first time")?;
|
||||
sql.close().await;
|
||||
|
||||
// Reopen the database
|
||||
let sql = Sql::new(dbfile);
|
||||
|
||||
// Test that we can't open encrypted database without a passphrase.
|
||||
assert!(sql.open(&t, "".to_string()).await.is_err());
|
||||
|
||||
// Now open the database with passpharse, it should succeed.
|
||||
sql.check_passphrase("foo".to_string()).await?;
|
||||
sql.open(&t, "foo".to_string())
|
||||
.await
|
||||
.context("failed to open the database second time")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sql_change_passphrase() -> Result<()> {
|
||||
use tempfile::tempdir;
|
||||
|
||||
// The context is used only for logging.
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// Create a separate empty database for testing.
|
||||
let dir = tempdir()?;
|
||||
let dbfile = dir.path().join("testdb.sqlite");
|
||||
let sql = Sql::new(dbfile.clone());
|
||||
|
||||
sql.open(&t, "foo".to_string())
|
||||
.await
|
||||
.context("failed to open the database first time")?;
|
||||
sql.close().await;
|
||||
|
||||
// Change the passphrase from "foo" to "bar".
|
||||
let sql = Sql::new(dbfile.clone());
|
||||
sql.open(&t, "foo".to_string())
|
||||
.await
|
||||
.context("failed to open the database second time")?;
|
||||
sql.change_passphrase("bar".to_string())
|
||||
.await
|
||||
.context("failed to change passphrase")?;
|
||||
|
||||
// Test that at least two connections are still working.
|
||||
// This ensures that not only the connection which changed the password is working,
|
||||
// but other connections as well.
|
||||
{
|
||||
let lock = sql.pool.read().await;
|
||||
let pool = lock.as_ref().unwrap();
|
||||
let query_only = true;
|
||||
let conn1 = pool.get(query_only).await?;
|
||||
let conn2 = pool.get(query_only).await?;
|
||||
conn1
|
||||
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
|
||||
.unwrap();
|
||||
conn2
|
||||
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
sql.close().await;
|
||||
|
||||
let sql = Sql::new(dbfile);
|
||||
|
||||
// Test that old passphrase is not working.
|
||||
assert!(sql.open(&t, "foo".to_string()).await.is_err());
|
||||
|
||||
// Open the database with the new passphrase.
|
||||
sql.check_passphrase("bar".to_string()).await?;
|
||||
sql.open(&t, "bar".to_string())
|
||||
.await
|
||||
.context("failed to open the database third time")?;
|
||||
sql.close().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_query_only() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
+3
-3
@@ -69,7 +69,7 @@ struct ContactStat {
|
||||
#[serde(skip_serializing_if = "is_false", rename = "direct_chat")]
|
||||
single_chat: bool,
|
||||
|
||||
last_seen: i64,
|
||||
last_seen: u64,
|
||||
|
||||
/// Whether the contact was established after stats-sending was enabled
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
@@ -312,7 +312,7 @@ async fn ensure_last_old_contact_id(context: &Context) -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let last_contact_id: u32 = context
|
||||
let last_contact_id: u64 = context
|
||||
.sql
|
||||
.query_get_value("SELECT MAX(id) FROM contacts", ())
|
||||
.await?
|
||||
@@ -436,7 +436,7 @@ async fn get_contact_stats(context: &Context, last_old_contact: u32) -> Result<V
|
||||
|row| {
|
||||
let id = row.get(0)?;
|
||||
let encrypted: bool = row.get(1)?;
|
||||
let last_seen: i64 = row.get(2)?;
|
||||
let last_seen: u64 = row.get(2)?;
|
||||
let bot: bool = row.get(3)?;
|
||||
|
||||
Ok(ContactStat {
|
||||
|
||||
+8
-16
@@ -56,18 +56,16 @@ pub async fn get_storage_usage(ctx: &Context) -> Result<StorageUsage> {
|
||||
let blobdir_size =
|
||||
tokio::task::spawn_blocking(move || get_blobdir_storage_usage(&context_clone));
|
||||
|
||||
let page_size: i64 = ctx
|
||||
let page_size: u64 = ctx
|
||||
.sql
|
||||
.query_get_value("PRAGMA page_size", ())
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let page_size = u64::try_from(page_size)?;
|
||||
let page_count: i64 = ctx
|
||||
let page_count: u64 = ctx
|
||||
.sql
|
||||
.query_get_value("PRAGMA page_count", ())
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let page_count = u64::try_from(page_count)?;
|
||||
|
||||
let mut largest_tables = ctx
|
||||
.sql
|
||||
@@ -80,8 +78,7 @@ pub async fn get_storage_usage(ctx: &Context) -> Result<StorageUsage> {
|
||||
(),
|
||||
|row| {
|
||||
let name: String = row.get(0)?;
|
||||
let size: i64 = row.get(1)?;
|
||||
let size: u64 = u64::try_from(size)?;
|
||||
let size: u64 = row.get(1)?;
|
||||
Ok((name, size, None))
|
||||
},
|
||||
)
|
||||
@@ -89,13 +86,12 @@ pub async fn get_storage_usage(ctx: &Context) -> Result<StorageUsage> {
|
||||
|
||||
for row in &mut largest_tables {
|
||||
let name = &row.0;
|
||||
let row_count: Option<i64> = ctx
|
||||
let row_count: Result<Option<u64>> = ctx
|
||||
.sql
|
||||
// SECURITY: the table name comes from the db, not from the user
|
||||
.query_get_value(&format!("SELECT COUNT(*) FROM {name}"), ())
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
row.2 = row_count.map(|count| u64::try_from(count).unwrap_or_default());
|
||||
.await;
|
||||
row.2 = row_count.unwrap_or_default();
|
||||
}
|
||||
|
||||
let largest_webxdc_data = ctx
|
||||
@@ -107,12 +103,8 @@ pub async fn get_storage_usage(ctx: &Context) -> Result<StorageUsage> {
|
||||
(),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
let size: i64 = row.get(1)?;
|
||||
let count: i64 = row.get(2)?;
|
||||
|
||||
// This should never fail as the count cannot be negative.
|
||||
let size: u64 = u64::try_from(size)?;
|
||||
let count: u64 = u64::try_from(count)?;
|
||||
let size: u64 = row.get(1)?;
|
||||
let count: u64 = row.get(2)?;
|
||||
|
||||
Ok((msg_id, size, count))
|
||||
},
|
||||
|
||||
+3
-4
@@ -35,7 +35,7 @@ use crate::context::Context;
|
||||
use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::key::{self, DcKey, self_fingerprint};
|
||||
use crate::message::{Message, MessageState, MsgId};
|
||||
use crate::mimefactory::{self, MimeFactory};
|
||||
use crate::mimefactory;
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::pgp::SeipdVersion;
|
||||
use crate::receive_imf::{ReceivedMsg, receive_imf};
|
||||
@@ -852,9 +852,8 @@ ORDER BY id"
|
||||
|
||||
/// Receives a read receipt from `reader`, who received `msg`.
|
||||
pub async fn recv_mdn(&self, reader: &TestContext, msg: &Message) -> Result<()> {
|
||||
let mdn = MimeFactory::from_mdn(reader, msg.from_id, msg.rfc724_mid.clone(), vec![])
|
||||
.await?
|
||||
.render(reader, &reader.get_primary_self_addr().await?)
|
||||
let queued_mdn = mimefactory::mdn(reader, msg.from_id, &msg.rfc724_mid, vec![]).await?;
|
||||
let mdn = mimefactory::render_queued_mail_with_context(queued_mdn, reader)
|
||||
.await?
|
||||
.message;
|
||||
receive_imf(self, mdn.as_bytes(), false).await?;
|
||||
|
||||
@@ -220,6 +220,7 @@ async fn maybe_warn_on_bad_time(context: &Context, now: i64, known_past_timestam
|
||||
),
|
||||
Some(&mut msg),
|
||||
true,
|
||||
time(),
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
+13
-13
@@ -27,7 +27,7 @@ impl Context {
|
||||
Ok(param.parse().unwrap_or_default())
|
||||
},
|
||||
)?;
|
||||
let update = param.update_timestamp(scope, new_timestamp)?;
|
||||
let update = param.update_timestamp(scope, new_timestamp);
|
||||
if update {
|
||||
transaction.execute(
|
||||
"UPDATE contacts SET param=? WHERE id=?",
|
||||
@@ -57,7 +57,7 @@ impl ChatId {
|
||||
let param: String = row.get(0)?;
|
||||
Ok(param.parse().unwrap_or_default())
|
||||
})?;
|
||||
let update = param.update_timestamp(scope, new_timestamp)?;
|
||||
let update = param.update_timestamp(scope, new_timestamp);
|
||||
if update {
|
||||
transaction.execute(
|
||||
"UPDATE chats SET param=? WHERE id=?",
|
||||
@@ -73,13 +73,13 @@ impl ChatId {
|
||||
impl Params {
|
||||
/// Updates a param's timestamp in memory, if reasonable.
|
||||
/// Returns true if the caller shall update the settings belonging to the scope.
|
||||
pub(crate) fn update_timestamp(&mut self, scope: Param, new_timestamp: i64) -> Result<bool> {
|
||||
pub(crate) fn update_timestamp(&mut self, scope: Param, new_timestamp: i64) -> bool {
|
||||
let old_timestamp = self.get_i64(scope).unwrap_or_default();
|
||||
if new_timestamp >= old_timestamp {
|
||||
self.set_i64(scope, new_timestamp);
|
||||
return Ok(true);
|
||||
return true;
|
||||
}
|
||||
Ok(false)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,18 +96,18 @@ mod tests {
|
||||
let mut params = Params::new();
|
||||
let ts = time();
|
||||
|
||||
assert!(params.update_timestamp(Param::LastSubject, ts)?);
|
||||
assert!(params.update_timestamp(Param::LastSubject, ts)?); // same timestamp -> update
|
||||
assert!(params.update_timestamp(Param::LastSubject, ts + 10)?);
|
||||
assert!(!params.update_timestamp(Param::LastSubject, ts)?); // `ts` is now too old
|
||||
assert!(!params.update_timestamp(Param::LastSubject, 0)?);
|
||||
assert!(params.update_timestamp(Param::LastSubject, ts));
|
||||
assert!(params.update_timestamp(Param::LastSubject, ts)); // same timestamp -> update
|
||||
assert!(params.update_timestamp(Param::LastSubject, ts + 10));
|
||||
assert!(!params.update_timestamp(Param::LastSubject, ts)); // `ts` is now too old
|
||||
assert!(!params.update_timestamp(Param::LastSubject, 0));
|
||||
assert_eq!(params.get_i64(Param::LastSubject).unwrap(), ts + 10);
|
||||
|
||||
assert!(params.update_timestamp(Param::GroupNameTimestamp, 0)?); // stay unset -> update ...
|
||||
assert!(params.update_timestamp(Param::GroupNameTimestamp, 0)?); // ... also on multiple calls
|
||||
assert!(params.update_timestamp(Param::GroupNameTimestamp, 0)); // stay unset -> update ...
|
||||
assert!(params.update_timestamp(Param::GroupNameTimestamp, 0)); // ... also on multiple calls
|
||||
assert_eq!(params.get_i64(Param::GroupNameTimestamp).unwrap(), 0);
|
||||
|
||||
assert!(!params.update_timestamp(Param::AvatarTimestamp, -1)?);
|
||||
assert!(!params.update_timestamp(Param::AvatarTimestamp, -1));
|
||||
assert_eq!(params.get_i64(Param::AvatarTimestamp), None);
|
||||
|
||||
Ok(())
|
||||
|
||||
+3
-3
@@ -350,7 +350,7 @@ impl Context {
|
||||
if let Some(ref document) = status_update_item.document
|
||||
&& instance
|
||||
.param
|
||||
.update_timestamp(Param::WebxdcDocumentTimestamp, timestamp)?
|
||||
.update_timestamp(Param::WebxdcDocumentTimestamp, timestamp)
|
||||
{
|
||||
instance.param.set(Param::WebxdcDocument, document);
|
||||
param_changed = true;
|
||||
@@ -359,10 +359,10 @@ impl Context {
|
||||
if let Some(ref summary) = status_update_item.summary
|
||||
&& instance
|
||||
.param
|
||||
.update_timestamp(Param::WebxdcSummaryTimestamp, timestamp)?
|
||||
.update_timestamp(Param::WebxdcSummaryTimestamp, timestamp)
|
||||
{
|
||||
let summary = sanitize_bidi_characters(summary);
|
||||
instance.param.set(Param::WebxdcSummary, summary.clone());
|
||||
instance.param.set(Param::WebxdcSummary, summary);
|
||||
param_changed = true;
|
||||
}
|
||||
|
||||
|
||||
+12
-14
@@ -1201,30 +1201,28 @@ async fn test_get_webxdc_blob_with_subdirs() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_parse_webxdc_manifest() -> Result<()> {
|
||||
let result = parse_webxdc_manifest(r#"key = syntax error"#.as_bytes());
|
||||
let result = parse_webxdc_manifest(br#"key = syntax error"#);
|
||||
assert!(result.is_err());
|
||||
|
||||
let manifest = parse_webxdc_manifest(r#"no_name = "no name, no icon""#.as_bytes())?;
|
||||
let manifest = parse_webxdc_manifest(br#"no_name = "no name, no icon""#)?;
|
||||
assert_eq!(manifest.name, None);
|
||||
|
||||
let manifest = parse_webxdc_manifest(r#"name = "name, no icon""#.as_bytes())?;
|
||||
let manifest = parse_webxdc_manifest(br#"name = "name, no icon""#)?;
|
||||
assert_eq!(manifest.name, Some("name, no icon".to_string()));
|
||||
|
||||
let manifest = parse_webxdc_manifest(
|
||||
r#"name = "foo"
|
||||
icon = "bar""#
|
||||
.as_bytes(),
|
||||
br#"name = "foo"
|
||||
icon = "bar""#,
|
||||
)?;
|
||||
assert_eq!(manifest.name, Some("foo".to_string()));
|
||||
|
||||
let manifest = parse_webxdc_manifest(
|
||||
r#"name = "foz"
|
||||
br#"name = "foz"
|
||||
icon = "baz"
|
||||
add_item = "that should be just ignored"
|
||||
|
||||
[section]
|
||||
sth_for_the = "future""#
|
||||
.as_bytes(),
|
||||
sth_for_the = "future""#,
|
||||
)?;
|
||||
assert_eq!(manifest.name, Some("foz".to_string()));
|
||||
Ok(())
|
||||
@@ -1232,13 +1230,13 @@ sth_for_the = "future""#
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_parse_webxdc_manifest_min_api() -> Result<()> {
|
||||
let manifest = parse_webxdc_manifest(r#"min_api = 3"#.as_bytes())?;
|
||||
let manifest = parse_webxdc_manifest(br#"min_api = 3"#)?;
|
||||
assert_eq!(manifest.min_api, Some(3));
|
||||
|
||||
let result = parse_webxdc_manifest(r#"min_api = "1""#.as_bytes());
|
||||
let result = parse_webxdc_manifest(br#"min_api = "1""#);
|
||||
assert!(result.is_err());
|
||||
|
||||
let result = parse_webxdc_manifest(r#"min_api = 1.2"#.as_bytes());
|
||||
let result = parse_webxdc_manifest(br#"min_api = 1.2"#);
|
||||
assert!(result.is_err());
|
||||
|
||||
Ok(())
|
||||
@@ -1246,10 +1244,10 @@ async fn test_parse_webxdc_manifest_min_api() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_parse_webxdc_manifest_source_code_url() -> Result<()> {
|
||||
let result = parse_webxdc_manifest(r#"source_code_url = 3"#.as_bytes());
|
||||
let result = parse_webxdc_manifest(br#"source_code_url = 3"#);
|
||||
assert!(result.is_err());
|
||||
|
||||
let manifest = parse_webxdc_manifest(r#"source_code_url = "https://foo.bar""#.as_bytes())?;
|
||||
let manifest = parse_webxdc_manifest(br#"source_code_url = "https://foo.bar""#)?;
|
||||
assert_eq!(
|
||||
manifest.source_code_url,
|
||||
Some("https://foo.bar".to_string())
|
||||
|
||||
Reference in New Issue
Block a user