Compare commits

...

50 Commits

Author SHA1 Message Date
Floris Bruynooghe
feb21b8731 Start refactoring peer channels to stop iroh when not needed 2025-07-16 14:42:54 +02:00
link2xt
a8a7cec376 refactor: use CancellationToken instead of a 1-message channel 2025-06-11 14:28:24 +00:00
d2weber
7f6beeeecb feat: put "biography" in the vCard (#6819)
Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com>
2025-06-11 10:08:18 -03:00
link2xt
15092407ea build: enable async-native-tls/vendored feature
OpenSSL is vendored, but because of rusqlite feature
transitively enabling vendoring feature.
This change makes vendoring explicit
even if we disable SQLCipher in the future.
2025-06-09 22:17:52 +00:00
dependabot[bot]
bd70d48cdf chore(cargo): bump criterion from 0.5.1 to 0.6.0
Bumps [criterion](https://github.com/bheisler/criterion.rs) from 0.5.1 to 0.6.0.
- [Changelog](https://github.com/bheisler/criterion.rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bheisler/criterion.rs/compare/0.5.1...0.6.0)

---
updated-dependencies:
- dependency-name: criterion
  dependency-version: 0.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Co-authored-by: iequidoo <dgreshilov@gmail.com>
2025-06-07 06:04:05 -03:00
iequidoo
ce04e904e2 fix: Sort multiple saved messages by timestamp (#6862) 2025-06-06 03:59:36 -03:00
link2xt
026ddbf9f1 build: upgrade parking_lot to 0.12.4 2025-06-05 20:15:37 +00:00
link2xt
628b178076 build: update cargo-bolero from 0.8.0 to 0.13.3
New `fuzz` profile is added
because cargo-bolero now requires it and uses
by default, while `--release` option is removed.

Instructions for running AFL are removed from the README
because it requires some system reconfiguration
and I did not test it this time.
2025-06-05 16:15:01 +00:00
WofWca
823a16e8e9 fix: fetch_url: return err on non 2xx reponses
The main reason for this change is the app picker
that Delta Chat clients use, which utilizes
the `fetch_url` function.
Sometimes we get an error from the server,
but we have no way to figure out that it's an error,
other than inspecting the body, which we don't (and shouldn't) do.
This results in us attempting to send webxdc apps
that are not even valid .zip files.

Another, arguably even worse thing is that
we also put the error responses to the cache,
so it's not easy to recover from such an error.

So, let's just return an error if the response code
is not a successful response code.
2025-06-04 23:28:17 +00:00
link2xt
407ec1311e docs: add more code style guide references 2025-06-04 19:03:51 +00:00
link2xt
b9667aae6b feat: better error for quoting a message from another chat 2025-06-04 18:28:35 +00:00
dependabot[bot]
806b437209 chore(cargo): bump tempfile from 3.19.1 to 3.20.0
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.19.1 to 3.20.0.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.19.1...v3.20.0)

---
updated-dependencies:
- dependency-name: tempfile
  dependency-version: 3.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 16:57:04 +00:00
dependabot[bot]
1a5232f863 chore(cargo): bump rustls-pki-types from 1.11.0 to 1.12.0
Bumps [rustls-pki-types](https://github.com/rustls/pki-types) from 1.11.0 to 1.12.0.
- [Release notes](https://github.com/rustls/pki-types/releases)
- [Commits](https://github.com/rustls/pki-types/compare/v/1.11.0...v/1.12.0)

---
updated-dependencies:
- dependency-name: rustls-pki-types
  dependency-version: 1.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 16:56:30 +00:00
dependabot[bot]
7ad119f126 chore(cargo): bump mail-builder from 0.4.2 to 0.4.3
Bumps [mail-builder](https://github.com/stalwartlabs/mail-builder) from 0.4.2 to 0.4.3.
- [Release notes](https://github.com/stalwartlabs/mail-builder/releases)
- [Changelog](https://github.com/stalwartlabs/mail-builder/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stalwartlabs/mail-builder/commits)

---
updated-dependencies:
- dependency-name: mail-builder
  dependency-version: 0.4.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 16:56:08 +00:00
dependabot[bot]
1682f4b252 chore(cargo): bump hyper-util from 0.1.11 to 0.1.13
Bumps [hyper-util](https://github.com/hyperium/hyper-util) from 0.1.11 to 0.1.13.
- [Release notes](https://github.com/hyperium/hyper-util/releases)
- [Changelog](https://github.com/hyperium/hyper-util/blob/master/CHANGELOG.md)
- [Commits](https://github.com/hyperium/hyper-util/compare/v0.1.11...v0.1.13)

---
updated-dependencies:
- dependency-name: hyper-util
  dependency-version: 0.1.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 00:46:22 +00:00
dependabot[bot]
6a320d545b chore(cargo): bump uuid from 1.16.0 to 1.17.0
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.16.0 to 1.17.0.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.16.0...v1.17.0)

---
updated-dependencies:
- dependency-name: uuid
  dependency-version: 1.17.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 00:45:22 +00:00
dependabot[bot]
e7aebd6fbc chore(cargo): bump rustyline from 15.0.0 to 16.0.0
Bumps [rustyline](https://github.com/kkawakam/rustyline) from 15.0.0 to 16.0.0.
- [Release notes](https://github.com/kkawakam/rustyline/releases)
- [Changelog](https://github.com/kkawakam/rustyline/blob/master/History.md)
- [Commits](https://github.com/kkawakam/rustyline/compare/v15.0.0...v16.0.0)

---
updated-dependencies:
- dependency-name: rustyline
  dependency-version: 16.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 00:44:42 +00:00
dependabot[bot]
8189abd660 chore(cargo): bump num_cpus from 1.16.0 to 1.17.0
Bumps [num_cpus](https://github.com/seanmonstar/num_cpus) from 1.16.0 to 1.17.0.
- [Release notes](https://github.com/seanmonstar/num_cpus/releases)
- [Changelog](https://github.com/seanmonstar/num_cpus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/num_cpus/compare/v1.16.0...v1.17.0)

---
updated-dependencies:
- dependency-name: num_cpus
  dependency-version: 1.17.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 00:44:23 +00:00
dependabot[bot]
5ded153ae4 chore(cargo): bump tokio from 1.44.2 to 1.45.1
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.44.2 to 1.45.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.44.2...tokio-1.45.1)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.45.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 00:43:58 +00:00
dependabot[bot]
2fd5507c00 chore(cargo): bump brotli from 8.0.0 to 8.0.1
Bumps [brotli](https://github.com/dropbox/rust-brotli) from 8.0.0 to 8.0.1.
- [Release notes](https://github.com/dropbox/rust-brotli/releases)
- [Commits](https://github.com/dropbox/rust-brotli/commits)

---
updated-dependencies:
- dependency-name: brotli
  dependency-version: 8.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 00:43:48 +00:00
link2xt
becb83faf1 fix: create group chats unprotected on verification error 2025-05-31 12:54:44 +00:00
link2xt
32263b4574 fix: ignore verification error if the chat is not protected yet
If we receive a message from non-verified contact
in a non-protected chat with a Chat-Verified header,
there is no need to upgrade the chat
to verified and display an error.

If it was an attack, an attacker could
just not send the Chat-Verified header.
Most of the time, however, it is just
message reordering.
2025-05-31 12:54:44 +00:00
link2xt
fd3e48dcb2 chore: run npm run prettier:fix 2025-05-29 15:13:42 +00:00
link2xt
69573cd735 chore: update deltachat-jsonrpc JS dependencies 2025-05-29 15:13:42 +00:00
Friedel Ziegelmayer
5c2af42cdd build: update to rPGP 0.16.0 (#6719)
Co-authored-by: Heiko Schaefer <heiko@schaefer.name>
Co-authored-by: link2xt <link2xt@testrun.org>
2025-05-29 13:06:18 +00:00
link2xt
42975b2ff3 chore: expect clippy::large_enum_variant 2025-05-29 11:58:11 +00:00
link2xt
c7063c00f7 ci: use installed toolchain to lint Rust 2025-05-29 11:58:11 +00:00
link2xt
89df9536e9 fix: reduce the scope of the last_full_folder_scan lock in scan_folders
This makes it easier to ensure that holding this lock
does not result in deadlocks.
2025-05-28 15:46:01 +00:00
Sebastian Klähn
0e45c2246f fix: remove faulty test (#6880)
The test was still WIP but got merged together with the fix. I suggest
to keep the fix in main and add the test in a follow-up RP. The test
should suffice becaues I tested it manually.
2025-05-28 17:43:05 +02:00
Sebastian Klähn
81a6afde15 Fix(jsonrpc): Do not error on missign webxdc info (#6866)
When an invalid webxdc is set as draft, json-rpc's `get_draft` fails,
because `get_webxdc_info` which it calls, fails because the zip reader
can not read a non-zip file. With this change, any error occurring in
`get_webxdc_info` is ignored and the None-variant is returned instead. I
also added a test, that setting invalid xdcs is draft is fine core-wise
and checked that the input field stays responsive when a fake.xdc
produced like in #6826 is added to draft

close #6826
2025-05-28 16:29:54 +02:00
link2xt
adcc8a919c build: update Doxygen config and layout file 2025-05-26 18:19:26 +00:00
bjoern
a24e6d4278 feat: sort apps by recently-updated (#6875)
closes #6873 , see there for reasoning.

tested that on iOS already, works like a charm - and was much easier
than expected as @iequidoo already updated `timestamp_rcvd` on status
updates in https://github.com/chatmail/core/pull/5388

~~a test is missing, ordering is not tested at all, will check if that
is doable reasonably easy~~ EDIT: added a test
2025-05-26 18:33:48 +02:00
dependabot[bot]
776b2247dd chore(deps): bump astral-sh/setup-uv from 5 to 6
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 5 to 6.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v5...v6)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-26 16:31:29 +00:00
link2xt
37dc1f5ca0 api!: deprecate DC_GCL_VERIFIED_ONLY 2025-05-20 16:14:43 +00:00
link2xt
a68ddab703 chore: apply beta clippy fixes 2025-05-20 14:09:07 +00:00
link2xt
877f873910 feat: add more IMAP logging
E.g. log when the folder is selected.
2025-05-19 08:25:05 +00:00
link2xt
53fa0147ae docs: update Imap.prepare() documentation 2025-05-19 08:25:05 +00:00
link2xt
7655c5b150 docs: update Imap.connect() documentation 2025-05-19 08:25:05 +00:00
link2xt
235b625f71 refactor: remove explicit lock drop at the end of scope 2025-05-19 08:25:05 +00:00
dependabot[bot]
014b0024a0 chore(deps): bump dependabot/fetch-metadata from 2.3.0 to 2.4.0
Bumps [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) from 2.3.0 to 2.4.0.
- [Release notes](https://github.com/dependabot/fetch-metadata/releases)
- [Commits](https://github.com/dependabot/fetch-metadata/compare/v2.3.0...v2.4.0)

---
updated-dependencies:
- dependency-name: dependabot/fetch-metadata
  dependency-version: 2.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-17 17:49:27 +00:00
dependabot[bot]
b0508e661a Merge pull request #6827 from chatmail/dependabot/cargo/shadowsocks-1.23.1 2025-05-16 04:30:39 +00:00
link2xt
ab3cd6a8f7 chore(deny.toml): add exception for deplicate spin 0.9.8 dependency 2025-05-16 04:14:27 +00:00
dependabot[bot]
85461204c5 chore(cargo): bump shadowsocks from 1.22.0 to 1.23.1
Bumps [shadowsocks](https://github.com/shadowsocks/shadowsocks-rust) from 1.22.0 to 1.23.1.
- [Release notes](https://github.com/shadowsocks/shadowsocks-rust/releases)
- [Commits](https://github.com/shadowsocks/shadowsocks-rust/compare/v1.22.0...v1.23.1)

---
updated-dependencies:
- dependency-name: shadowsocks
  dependency-version: 1.23.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-16 03:07:34 +00:00
link2xt
3abf2b5227 build: increase MSRV to 1.85.0 2025-05-16 03:03:58 +00:00
link2xt
0d5d7032fe build: nix flake update nixpkgs 2025-05-16 03:03:58 +00:00
link2xt
c48b04ab99 ci(nix): test build on macOS without cross-compilation 2025-05-16 03:03:58 +00:00
link2xt
eaa30dbe21 build: nix flake update fenix 2025-05-16 03:03:58 +00:00
link2xt
bb0f812f71 ci: update Rust to 1.87.0 2025-05-16 03:03:58 +00:00
link2xt
4c287075da fix: do not allow chat creation if decryption failed 2025-05-15 18:02:19 +00:00
link2xt
09d18f9097 test: fixup for test_restore_backup_after_60_days 2025-05-15 18:02:19 +00:00
73 changed files with 2480 additions and 1062 deletions

View File

@@ -20,10 +20,10 @@ permissions: {}
env:
RUSTFLAGS: -Dwarnings
RUST_VERSION: 1.86.0
RUST_VERSION: 1.87.0
# Minimum Supported Rust Version
MSRV: 1.82.0
MSRV: 1.85.0
jobs:
lint_rust:
@@ -36,6 +36,8 @@ jobs:
persist-credentials: false
- name: Install rustfmt and clippy
run: rustup toolchain install $RUST_VERSION --profile minimal --component rustfmt --component clippy
- run: rustup override set $RUST_VERSION
shell: bash
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2
- name: Run rustfmt

View File

@@ -14,7 +14,7 @@ jobs:
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v2.3.0
uses: dependabot/fetch-metadata@v2.4.0
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Approve a PR

View File

@@ -95,9 +95,10 @@ jobs:
fail-fast: false
matrix:
installable:
- deltachat-rpc-server-aarch64-darwin
- deltachat-rpc-server
# Fails to bulid
# - deltachat-rpc-server-aarch64-darwin
# - deltachat-rpc-server-x86_64-darwin
steps:
- uses: actions/checkout@v4

View File

@@ -19,7 +19,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v5
uses: astral-sh/setup-uv@v6
- name: Run zizmor
run: uvx zizmor --format sarif . > results.sarif

1
.gitignore vendored
View File

@@ -53,3 +53,4 @@ result
# direnv
.envrc
.direnv
.aider*

416
Cargo.lock generated
View File

@@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "addr2line"
@@ -439,10 +439,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bitfield"
version = "0.17.0"
name = "bitfields"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f798d2d157e547aa99aab0967df39edd0b70307312b6f8bd2848e6abe40896e0"
checksum = "a1d84268bbf9b487d31fe4b849edbefcd3911422d7a07de855a2da1f70ab3d1c"
dependencies = [
"bitfields-impl",
]
[[package]]
name = "bitfields-impl"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07c93edde7bb4416c35c85048e34f78999dcb47d199bde3b1d79286156f3e2fb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"thiserror 2.0.12",
]
[[package]]
name = "bitflags"
@@ -456,6 +471,18 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
[[package]]
name = "bitvec"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
dependencies = [
"funty",
"radium",
"tap",
"wyz",
]
[[package]]
name = "blake2"
version = "0.10.6"
@@ -508,9 +535,9 @@ dependencies = [
[[package]]
name = "bolero"
version = "0.8.0"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3387d308f66ed222bdbb19c6ba06b1517168c4e45dc64051c5f1b4845db2901c"
checksum = "4e913ed74716cd68dc5be41c702327b1cc4ffc8f0b55945ae46fb015777007eb"
dependencies = [
"bolero-afl",
"bolero-engine",
@@ -519,14 +546,14 @@ dependencies = [
"bolero-kani",
"bolero-libfuzzer",
"cfg-if",
"rand 0.8.5",
"rand 0.9.0",
]
[[package]]
name = "bolero-afl"
version = "0.8.0"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "973bc6341b6a865dee93f17b78de4a100551014a527798ff1d7265d3bc0f7d89"
checksum = "d9bf4cbd0bacf9356d3c7e5d9d088480f2076ba3c595c15ee9a6a378cdd7b297"
dependencies = [
"bolero-engine",
"cc",
@@ -534,35 +561,38 @@ dependencies = [
[[package]]
name = "bolero-engine"
version = "0.8.1"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c506a476cea9e95f58c264b343ee279c353d93ceaebe98cbfb16e74bfaee2e2"
checksum = "05cae8c41807b046bb7005f52fa60c8f67787c1bf272242f0b84224853e04ceb"
dependencies = [
"anyhow",
"backtrace",
"bolero-generator",
"lazy_static",
"pretty-hex",
"rand 0.8.5",
"rand 0.9.0",
"rand_xoshiro",
]
[[package]]
name = "bolero-generator"
version = "0.8.0"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48d52eca8714d110e581cf17eeacf0d1a0d409d38a9e9ce07efeda6125f7febb"
checksum = "8e3ac7405f187921256faa515fa05ae02521103582a9d938410cefabe3a9a172"
dependencies = [
"bolero-generator-derive",
"either",
"rand_core 0.6.4",
"getrandom 0.3.3",
"rand_core 0.9.3",
"rand_xoshiro",
]
[[package]]
name = "bolero-generator-derive"
version = "0.8.0"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3c57c2a0967ad1a09ba4c2bf8f1c6b6db2f71e8c0db4fa280c65a0f6c249c3"
checksum = "9c56c2f8c1c0707d678bebb36168cfd523c45927bb8d9cb7567d3578fa428cbd"
dependencies = [
"proc-macro-crate 2.0.0",
"proc-macro2",
"quote",
"syn 1.0.109",
@@ -570,27 +600,27 @@ dependencies = [
[[package]]
name = "bolero-honggfuzz"
version = "0.8.0"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7996a3fa8d93652358b9b3b805233807168f49740a8bf91a531cd61e4da65355"
checksum = "9a118ef27295eddefadc6a99728ee698d1b18d2e80dc4777d21bee3385096ffd"
dependencies = [
"bolero-engine",
]
[[package]]
name = "bolero-kani"
version = "0.8.0"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "206879993fffa1cf2c703b1ef93b0febfa76bae85a0a5d4ae0ee6d99a2e3b74e"
checksum = "852ea5784a9f3e68bfd302ca80b8b863bce140593eb5770fee6ab110899c28fc"
dependencies = [
"bolero-engine",
]
[[package]]
name = "bolero-libfuzzer"
version = "0.8.0"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdc5547411b84703d9020914f15a7d709cfb738c72b5e0f5a499fe56b8465c98"
checksum = "858dc57c11725c52662501fa79fdbc6f7050339a05ca1bf1e587add0fed40d62"
dependencies = [
"bolero-engine",
"cc",
@@ -604,9 +634,9 @@ checksum = "102dbef1187b1893e6dfe05a774e79fd52265f49f214f6879c8ff49f52c8188b"
[[package]]
name = "brotli"
version = "8.0.0"
version = "8.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf19e729cdbd51af9a397fb9ef8ac8378007b797f8273cfbfdf45dcaa316167b"
checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
@@ -623,16 +653,6 @@ dependencies = [
"alloc-stdlib",
]
[[package]]
name = "bstr"
version = "1.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "buffer-redux"
version = "1.0.2"
@@ -1003,26 +1023,22 @@ dependencies = [
[[package]]
name = "criterion"
version = "0.5.1"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
checksum = "3bf7af66b0989381bd0be551bd7cc91912a655a58c6918420c9527b1fd8b4679"
dependencies = [
"anes",
"cast",
"ciborium",
"clap",
"criterion-plot",
"futures",
"is-terminal",
"itertools",
"itertools 0.13.0",
"num-traits",
"once_cell",
"oorandom",
"plotters",
"rayon",
"regex",
"serde",
"serde_derive",
"serde_json",
"tinytemplate",
"tokio",
@@ -1036,7 +1052,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
dependencies = [
"cast",
"itertools",
"itertools 0.10.5",
]
[[package]]
@@ -1141,7 +1157,7 @@ dependencies = [
"crypto_secretbox",
"curve25519-dalek",
"salsa20",
"serdect",
"serdect 0.2.0",
"subtle",
"zeroize",
]
@@ -1200,6 +1216,23 @@ dependencies = [
"syn 2.0.101",
]
[[package]]
name = "cx448"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4c0cf476284b03eb6c10e78787b21c7abb7d7d43cb2f02532ba6b831ed892fa"
dependencies = [
"crypto-bigint",
"elliptic-curve",
"pkcs8",
"rand_core 0.6.4",
"serdect 0.3.0",
"sha3",
"signature",
"subtle",
"zeroize",
]
[[package]]
name = "darling"
version = "0.20.10"
@@ -1526,7 +1559,16 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05"
dependencies = [
"derive_more-impl",
"derive_more-impl 1.0.0",
]
[[package]]
name = "derive_more"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
dependencies = [
"derive_more-impl 2.0.1",
]
[[package]]
@@ -1541,6 +1583,18 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "derive_more-impl"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"unicode-xid",
]
[[package]]
name = "des"
version = "0.8.1"
@@ -1650,9 +1704,9 @@ checksum = "feeef44e73baff3a26d371801df019877a9866a8c493d315ab00177843314f35"
[[package]]
name = "dynosaur"
version = "0.1.3"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1b7ff6b5c4d1df9f75c6bcce5cebfa480a448d298b0c83a07b3f5bb318ad10f"
checksum = "277b2cb52d2df4acece06bb16bc0bb0a006970c7bf504eac2d310927a6f65890"
dependencies = [
"dynosaur_derive",
"trait-variant",
@@ -1660,9 +1714,9 @@ dependencies = [
[[package]]
name = "dynosaur_derive"
version = "0.1.3"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a600ac649c596084163b9bcde422e9973a321dd190b9d8b15dde23986e53f425"
checksum = "7a4102713839a8c01c77c165bc38ef2e83948f6397fa1e1dcfacec0f07b149d3"
dependencies = [
"proc-macro2",
"quote",
@@ -1735,6 +1789,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
dependencies = [
"base16ct",
"base64ct",
"crypto-bigint",
"digest",
"ff",
@@ -1745,7 +1800,10 @@ dependencies = [
"pkcs8",
"rand_core 0.6.4",
"sec1",
"serde_json",
"serdect 0.2.0",
"subtle",
"tap",
"zeroize",
]
@@ -1930,6 +1988,7 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449"
dependencies = [
"bitvec",
"rand_core 0.6.4",
"subtle",
]
@@ -1965,6 +2024,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
dependencies = [
"crc32fast",
"libz-rs-sys",
"miniz_oxide",
]
@@ -2002,6 +2062,12 @@ dependencies = [
name = "format-flowed"
version = "1.0.0"
[[package]]
name = "funty"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "futures"
version = "0.3.31"
@@ -2027,7 +2093,7 @@ dependencies = [
"diatomic-waker",
"futures-core",
"pin-project-lite",
"spin",
"spin 0.9.8",
]
[[package]]
@@ -2313,9 +2379,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.3.9"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08"
[[package]]
name = "hex"
@@ -2550,12 +2616,13 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.11"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2"
checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8"
dependencies = [
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"http 1.1.0",
"http-body",
@@ -2867,7 +2934,7 @@ dependencies = [
"crypto_box",
"data-encoding",
"der",
"derive_more",
"derive_more 1.0.0",
"ed25519-dalek",
"futures-buffered",
"futures-util",
@@ -2921,7 +2988,7 @@ checksum = "f91ac4aaab68153d726c4e6b39c30f9f9253743f0e25664e52f4caeb46f48d11"
dependencies = [
"curve25519-dalek",
"data-encoding",
"derive_more",
"derive_more 1.0.0",
"ed25519-dalek",
"rand_core 0.6.4",
"serde",
@@ -2951,7 +3018,7 @@ dependencies = [
"anyhow",
"async-channel 2.3.1",
"bytes",
"derive_more",
"derive_more 1.0.0",
"ed25519-dalek",
"futures-concurrency",
"futures-lite",
@@ -3062,7 +3129,7 @@ dependencies = [
"bytes",
"cfg_aliases",
"data-encoding",
"derive_more",
"derive_more 1.0.0",
"getrandom 0.3.3",
"hickory-resolver",
"http 1.1.0",
@@ -3099,23 +3166,6 @@ dependencies = [
"z32",
]
[[package]]
name = "is-terminal"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "iter-read"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071ed4cc1afd86650602c7b11aa2e1ce30762a1c27193201cb5cee9c6ebb1294"
[[package]]
name = "itertools"
version = "0.10.5"
@@ -3125,6 +3175,15 @@ dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.10"
@@ -3179,7 +3238,7 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [
"spin",
"spin 0.9.8",
]
[[package]]
@@ -3216,6 +3275,15 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "libz-rs-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6489ca9bd760fe9642d7644e827b0c9add07df89857b0416ee15c1cc1a3b8c5a"
dependencies = [
"zlib-rs",
]
[[package]]
name = "linux-raw-sys"
version = "0.4.14"
@@ -3242,9 +3310,9 @@ checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
[[package]]
name = "lock_api"
version = "0.4.12"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
dependencies = [
"autocfg",
"scopeguard",
@@ -3305,9 +3373,9 @@ checksum = "9106e1d747ffd48e6be5bb2d97fa706ed25b144fbee4d5c02eae110cd8d6badd"
[[package]]
name = "mail-builder"
version = "0.4.2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5666db60ccc742381c715c8daf303e60d266f1d1c8b123ab3fe78b590edaf564"
checksum = "0926cff74776d4af100a95c90a6649486659526ce638bee6648ecc9c41051810"
[[package]]
name = "mailparse"
@@ -3416,7 +3484,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bb0e5d99e681ab3c938842b96fcb41bf8a7bb4bfdb11ccbd653a7e83e06c794"
dependencies = [
"cfg_aliases",
"derive_more",
"derive_more 1.0.0",
"futures-buffered",
"futures-lite",
"futures-util",
@@ -3453,7 +3521,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43fa9161ed44d30e9702fe42bd78693bceac0fed02f647da749f36109023d3a3"
dependencies = [
"proc-macro-crate",
"proc-macro-crate 3.2.0",
"proc-macro2",
"quote",
"syn 1.0.109",
@@ -3564,7 +3632,7 @@ dependencies = [
"atomic-waker",
"bytes",
"cfg_aliases",
"derive_more",
"derive_more 1.0.0",
"iroh-quinn-udp",
"js-sys",
"libc",
@@ -3599,9 +3667,9 @@ dependencies = [
[[package]]
name = "nix"
version = "0.29.0"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags 2.8.0",
"cfg-if",
@@ -3745,9 +3813,9 @@ dependencies = [
[[package]]
name = "num_cpus"
version = "1.16.0"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
dependencies = [
"hermit-abi",
"libc",
@@ -3768,7 +3836,7 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
dependencies = [
"proc-macro-crate",
"proc-macro-crate 3.2.0",
"proc-macro2",
"quote",
"syn 2.0.101",
@@ -3949,9 +4017,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]]
name = "parking_lot"
version = "0.12.3"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
dependencies = [
"lock_api",
"parking_lot_core",
@@ -3959,15 +4027,15 @@ dependencies = [
[[package]]
name = "parking_lot_core"
version = "0.9.9"
version = "0.9.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
dependencies = [
"cfg-if",
"libc",
"redox_syscall 0.4.1",
"redox_syscall 0.5.12",
"smallvec",
"windows-targets 0.48.5",
"windows-targets 0.52.6",
]
[[package]]
@@ -4059,21 +4127,22 @@ dependencies = [
[[package]]
name = "pgp"
version = "0.15.0"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30249ac8a98b356b473b04bc5358c75a260aa96a295d0743ce752fe7b173f235"
checksum = "f91d320242d9b686612b15526fe38711afdf856e112eaa4775ce25b0d9b12b11"
dependencies = [
"aead",
"aes",
"aes-gcm",
"aes-kw",
"argon2",
"base64",
"bitfield",
"bitfields",
"block-padding",
"blowfish",
"bstr",
"buffer-redux",
"byteorder",
"bytes",
"camellia",
"cast5",
"cfb-mode",
@@ -4082,8 +4151,9 @@ dependencies = [
"const-oid",
"crc24",
"curve25519-dalek",
"cx448",
"derive_builder",
"derive_more",
"derive_more 2.0.1",
"des",
"digest",
"dsa",
@@ -4096,7 +4166,6 @@ dependencies = [
"hex",
"hkdf",
"idea",
"iter-read",
"k256",
"log",
"md-5",
@@ -4109,6 +4178,7 @@ dependencies = [
"p384",
"p521",
"rand 0.8.5",
"regex",
"ripemd",
"rsa",
"sha1",
@@ -4117,7 +4187,7 @@ dependencies = [
"sha3",
"signature",
"smallvec",
"thiserror 2.0.12",
"snafu",
"twofish",
"x25519-dalek",
"zeroize",
@@ -4343,7 +4413,7 @@ checksum = "7d6db66007eac4a0ec8331d0d20c734bd64f6445d64bbaf0d0a27fea7a054e36"
dependencies = [
"base64",
"bytes",
"derive_more",
"derive_more 1.0.0",
"futures-lite",
"futures-util",
"hyper-util",
@@ -4441,9 +4511,9 @@ dependencies = [
[[package]]
name = "pretty-hex"
version = "0.3.0"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5"
checksum = "bbc83ee4a840062f368f9096d80077a9841ec117e17e7f700df81958f1451254"
[[package]]
name = "pretty_assertions"
@@ -4464,13 +4534,22 @@ dependencies = [
"elliptic-curve",
]
[[package]]
name = "proc-macro-crate"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8"
dependencies = [
"toml_edit 0.20.7",
]
[[package]]
name = "proc-macro-crate"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b"
dependencies = [
"toml_edit",
"toml_edit 0.22.20",
]
[[package]]
@@ -4640,6 +4719,12 @@ version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]]
name = "radium"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "radix_trie"
version = "0.2.1"
@@ -4719,6 +4804,15 @@ dependencies = [
"rand_core 0.6.4",
]
[[package]]
name = "rand_xoshiro"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41"
dependencies = [
"rand_core 0.9.3",
]
[[package]]
name = "ratelimit"
version = "1.0.0"
@@ -4774,6 +4868,15 @@ dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_syscall"
version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
dependencies = [
"bitflags 2.8.0",
]
[[package]]
name = "redox_users"
version = "0.5.0"
@@ -5063,11 +5166,12 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
version = "1.11.0"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
dependencies = [
"web-time",
"zeroize",
]
[[package]]
@@ -5089,9 +5193,9 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
[[package]]
name = "rustyline"
version = "15.0.0"
version = "16.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ee1e066dc922e513bda599c6ccb5f3bb2b0ea5870a579448f2622993f0a9a2f"
checksum = "62fd9ca5ebc709e8535e8ef7c658eb51457987e48c98ead2be482172accc408d"
dependencies = [
"bitflags 2.8.0",
"cfg-if",
@@ -5198,6 +5302,7 @@ dependencies = [
"der",
"generic-array",
"pkcs8",
"serdect 0.2.0",
"subtle",
"zeroize",
]
@@ -5339,6 +5444,16 @@ dependencies = [
"serde",
]
[[package]]
name = "serdect"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f42f67da2385b51a5f9652db9c93d78aeaf7610bf5ec366080b6de810604af53"
dependencies = [
"base16ct",
"serde",
]
[[package]]
name = "sha-1"
version = "0.10.1"
@@ -5401,9 +5516,9 @@ dependencies = [
[[package]]
name = "shadowsocks"
version = "1.22.0"
version = "1.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1678a9acd37add020f89bfe05d45b9b8a6e8ad5d09f54ac2af3e0dcf0557b481"
checksum = "e78db9c9912c90ea7487f49bc149b329b535806bfa12b740fbade73f573a3d9f"
dependencies = [
"aes",
"base64",
@@ -5419,14 +5534,14 @@ dependencies = [
"once_cell",
"percent-encoding",
"pin-project",
"rand 0.8.5",
"rand 0.9.0",
"sendfd",
"serde",
"serde_json",
"serde_urlencoded",
"shadowsocks-crypto",
"socket2",
"spin",
"spin 0.10.0",
"thiserror 2.0.12",
"tokio",
"tokio-tfo",
@@ -5437,9 +5552,9 @@ dependencies = [
[[package]]
name = "shadowsocks-crypto"
version = "0.5.8"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc77ecb3a97509d22751b76665894fcffad2d10df8758f4e3f20c92ccde6bf4f"
checksum = "bda401a0ad32c82981d8862f2795713618de9bbf9768f03c17d9d145c6d805df"
dependencies = [
"aes",
"aes-gcm",
@@ -5449,7 +5564,7 @@ dependencies = [
"chacha20poly1305",
"hkdf",
"md-5",
"rand 0.8.5",
"rand 0.9.0",
"ring-compat",
"sha1",
]
@@ -5566,6 +5681,12 @@ name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]]
name = "spin"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
dependencies = [
"lock_api",
]
@@ -5781,10 +5902,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
[[package]]
name = "tempfile"
version = "3.19.1"
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tempfile"
version = "3.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
dependencies = [
"fastrand",
"getrandom 0.3.3",
@@ -5938,9 +6065,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.44.2"
version = "1.45.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48"
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
dependencies = [
"backtrace",
"bytes",
@@ -6076,7 +6203,7 @@ dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
"toml_edit 0.22.20",
]
[[package]]
@@ -6088,6 +6215,17 @@ dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.20.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81"
dependencies = [
"indexmap",
"toml_datetime",
"winnow 0.5.40",
]
[[package]]
name = "toml_edit"
version = "0.22.20"
@@ -6098,7 +6236,7 @@ dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
"winnow 0.6.26",
]
[[package]]
@@ -6354,12 +6492,14 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.16.0"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
dependencies = [
"getrandom 0.3.3",
"js-sys",
"serde",
"wasm-bindgen",
]
[[package]]
@@ -6971,6 +7111,15 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winnow"
version = "0.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
dependencies = [
"memchr",
]
[[package]]
name = "winnow"
version = "0.6.26"
@@ -7045,6 +7194,15 @@ dependencies = [
"web-sys",
]
[[package]]
name = "wyz"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
dependencies = [
"tap",
]
[[package]]
name = "x25519-dalek"
version = "2.0.1"
@@ -7282,6 +7440,12 @@ dependencies = [
"syn 2.0.101",
]
[[package]]
name = "zlib-rs"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "868b928d7949e09af2f6086dfc1e01936064cc7a819253bce650d4e2a2d63ba8"
[[package]]
name = "zune-core"
version = "0.4.12"

View File

@@ -3,7 +3,7 @@ name = "deltachat"
version = "1.159.5"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.82"
rust-version = "1.85"
repository = "https://github.com/chatmail/core"
[profile.dev]
@@ -18,6 +18,9 @@ opt-level = 1
debug = 1
opt-level = 0
[profile.fuzz]
inherits = "test"
# Always optimize dependencies.
# This does not apply to crates in the workspace.
# <https://doc.rust-lang.org/cargo/reference/profiles.html#overrides>
@@ -60,21 +63,21 @@ hickory-resolver = "0.25.2"
http-body-util = "0.1.3"
humansize = "2"
hyper = "1"
hyper-util = "0.1.11"
hyper-util = "0.1.13"
image = { version = "0.25.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh-gossip = { version = "0.35", default-features = false, features = ["net"] }
iroh = { version = "0.35", default-features = false }
kamadak-exif = "0.6.1"
libc = { workspace = true }
mail-builder = { version = "0.4.2", default-features = false }
mail-builder = { version = "0.4.3", default-features = false }
mailparse = { workspace = true }
mime = "0.3.17"
num_cpus = "1.16"
num_cpus = "1.17"
num-derive = "0.4"
num-traits = { workspace = true }
parking_lot = "0.12"
parking_lot = "0.12.4"
percent-encoding = "2.3"
pgp = { version = "0.15.0", default-features = false }
pgp = { version = "0.16.0", default-features = false }
pin-project = "1"
qrcodegen = "1.7.0"
quick-xml = "0.37"
@@ -83,7 +86,7 @@ rand = { workspace = true }
regex = { workspace = true }
rusqlite = { workspace = true, features = ["sqlcipher"] }
rust-hsluv = "0.1"
rustls-pki-types = "1.11.0"
rustls-pki-types = "1.12.0"
rustls = { version = "0.23.22", default-features = false }
sanitize-filename = { workspace = true }
serde_json = { workspace = true }
@@ -91,7 +94,7 @@ serde_urlencoded = "0.7.1"
serde = { workspace = true, features = ["derive"] }
sha-1 = "0.10"
sha2 = "0.10"
shadowsocks = { version = "1.22.0", default-features = false, features = ["aead-cipher", "aead-cipher-2022"] }
shadowsocks = { version = "1.23.1", default-features = false, features = ["aead-cipher", "aead-cipher-2022"] }
smallvec = "1.15.0"
strum = "0.27"
strum_macros = "0.27"
@@ -112,7 +115,7 @@ blake3 = "1.8.2"
[dev-dependencies]
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
criterion = { version = "0.5.1", features = ["async_tokio"] }
criterion = { version = "0.6.0", features = ["async_tokio"] }
futures-lite = { workspace = true }
log = { workspace = true }
nu-ansi-term = { workspace = true }
@@ -190,7 +193,7 @@ rusqlite = "0.32"
sanitize-filename = "0.5"
serde = "1.0"
serde_json = "1"
tempfile = "3.19.1"
tempfile = "3.20.0"
thiserror = "2"
tokio = "1"
tokio-util = "0.7.14"
@@ -201,7 +204,8 @@ yerpc = "0.6.4"
default = ["vendored"]
internals = []
vendored = [
"rusqlite/bundled-sqlcipher-vendored-openssl"
"rusqlite/bundled-sqlcipher-vendored-openssl",
"async-native-tls/vendored"
]
[lints.rust]

View File

@@ -156,13 +156,13 @@ $ cargo test -- --ignored
Install [`cargo-bolero`](https://github.com/camshaft/bolero) with
```sh
$ cargo install cargo-bolero@0.8.0
$ cargo install cargo-bolero
```
Run fuzzing tests with
```sh
$ cd fuzz
$ cargo bolero test fuzz_mailparse --release=false -s NONE
$ cargo bolero test fuzz_mailparse -s NONE
```
Corpus is created at `fuzz/fuzz_targets/corpus`,
@@ -170,11 +170,6 @@ you can add initial inputs there.
For `fuzz_mailparse` target corpus can be populated with
`../test-data/message/*.eml`.
To run with AFL instead of libFuzzer:
```sh
$ cargo bolero test fuzz_format_flowed --release=false -e afl -s NONE
```
## Features
- `vendored`: When using Openssl for TLS, this bundles a vendored version.

View File

@@ -84,6 +84,13 @@ This allows to run `RUST_BACKTRACE=1 cargo test`
and get a backtrace with line numbers in resultified tests
which return `anyhow::Result`.
`unwrap` and `expect` are not used in the library
because panics are difficult to debug on user devices.
However, in the tests `.expect` may be used.
Follow
<https://doc.rust-lang.org/core/error/index.html#common-message-styles>
for `.expect` message style.
## Logging
For logging, use `info!`, `warn!` and `error!` macros.
@@ -96,3 +103,17 @@ Format anyhow errors with `{:#}` to print all the contexts like this:
```
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
```
## Documentation comments
All public modules, methods and fields should be documented.
This is checked by [`missing_docs`](https://doc.rust-lang.org/rustdoc/lints.html#missing_docs) lint.
Private items do not have to be documented,
but CI uses `cargo doc --document-private-items`
to build the documentation,
so it is preferred that new items
are documented.
Follow Rust guidelines for the documentation comments:
<https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#summary-sentence>

View File

@@ -1,5 +1,7 @@
#![recursion_limit = "256"]
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use std::hint::black_box;
use criterion::{criterion_group, criterion_main, Criterion};
use deltachat::contact::Contact;
use deltachat::context::Context;
use deltachat::stock_str::StockStrings;

View File

@@ -1,7 +1,8 @@
#![recursion_limit = "256"]
use std::hint::black_box;
use std::path::PathBuf;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use criterion::{criterion_group, criterion_main, Criterion};
use deltachat::accounts::Accounts;
use tempfile::tempdir;

View File

@@ -1,7 +1,8 @@
#![recursion_limit = "256"]
use std::hint::black_box;
use std::path::Path;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use criterion::{criterion_group, criterion_main, Criterion};
use deltachat::chat::{self, ChatId};
use deltachat::chatlist::Chatlist;
use deltachat::context::Context;

View File

@@ -1,7 +1,8 @@
#![recursion_limit = "256"]
use std::hint::black_box;
use std::path::Path;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use criterion::{criterion_group, criterion_main, Criterion};
use deltachat::chatlist::Chatlist;
use deltachat::context::Context;
use deltachat::stock_str::StockStrings;

View File

@@ -1,7 +1,8 @@
#![recursion_limit = "256"]
use std::hint::black_box;
use std::path::Path;
use criterion::{black_box, criterion_group, criterion_main, BatchSize, Criterion};
use criterion::{criterion_group, criterion_main, BatchSize, Criterion};
use deltachat::chat::{self, ChatId};
use deltachat::chatlist::Chatlist;
use deltachat::context::Context;

View File

@@ -1,7 +1,8 @@
#![recursion_limit = "256"]
use std::hint::black_box;
use std::path::PathBuf;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use criterion::{criterion_group, criterion_main, Criterion};
use deltachat::{
config::Config,
context::Context,

View File

@@ -1,7 +1,8 @@
#![recursion_limit = "256"]
use std::hint::black_box;
use std::path::Path;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use criterion::{criterion_group, criterion_main, Criterion};
use deltachat::context::Context;
use deltachat::stock_str::StockStrings;
use deltachat::Events;

View File

@@ -20,6 +20,8 @@ pub struct VcardContact {
pub key: Option<String>,
/// The contact's profile image (=avatar) in Base64, vcard property `photo`
pub profile_image: Option<String>,
/// The biography, stored in the vcard property `note`
pub biography: Option<String>,
/// The timestamp when the vcard was created / last updated, vcard property `rev`
pub timestamp: Result<i64>,
}
@@ -60,6 +62,9 @@ pub fn make_vcard(contacts: &[VcardContact]) -> String {
if let Some(profile_image) = &c.profile_image {
res += &format!("PHOTO:data:image/jpeg;base64,{profile_image}\r\n");
}
if let Some(biography) = &c.biography {
res += &format!("NOTE:{biography}\r\n");
}
if let Some(timestamp) = format_timestamp(c) {
res += &format!("REV:{timestamp}\r\n");
}
@@ -186,6 +191,7 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
let mut addr = None;
let mut key = None;
let mut photo = None;
let mut biography = None;
let mut datetime = None;
for mut line in lines.by_ref() {
@@ -205,6 +211,8 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
key.get_or_insert(k);
} else if let Some(p) = base64_photo(line) {
photo.get_or_insert(p);
} else if let Some((_params, bio)) = vcard_property(line, "note") {
biography.get_or_insert(bio);
} else if let Some((_params, rev)) = vcard_property(line, "rev") {
datetime.get_or_insert(rev);
} else if line.eq_ignore_ascii_case("END:VCARD") {
@@ -216,6 +224,7 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
addr,
key: key.map(|s| s.to_string()),
profile_image: photo.map(|s| s.to_string()),
biography: biography.map(|b| b.to_owned()),
timestamp: datetime
.context("No timestamp in vcard")
.and_then(parse_datetime),

View File

@@ -91,6 +91,7 @@ fn test_make_and_parse_vcard() {
authname: "Alice Wonderland".to_string(),
key: Some("[base64-data]".to_string()),
profile_image: Some("image in Base64".to_string()),
biography: Some("Hi, I'm Alice".to_string()),
timestamp: Ok(1713465762),
},
VcardContact {
@@ -98,6 +99,7 @@ fn test_make_and_parse_vcard() {
authname: "".to_string(),
key: None,
profile_image: None,
biography: None,
timestamp: Ok(0),
},
];
@@ -108,6 +110,7 @@ fn test_make_and_parse_vcard() {
FN:Alice Wonderland\r\n\
KEY:data:application/pgp-keys;base64,[base64-data]\r\n\
PHOTO:data:image/jpeg;base64,image in Base64\r\n\
NOTE:Hi, I'm Alice\r\n\
REV:20240418T184242Z\r\n\
END:VCARD\r\n",
"BEGIN:VCARD\r\n\

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
<doxygenlayout version="1.0">
<!-- Generated by doxygen 1.8.20 -->
<?xml version="1.0" encoding="UTF-8"?>
<doxygenlayout version="2.0">
<!-- Generated by doxygen 1.13.2 -->
<!-- Navigation index tabs for HTML output -->
<navindex>
<tab type="mainpage" visible="yes" title=""/>
@@ -11,10 +12,16 @@
</tab>
<tab type="topics" visible="yes" title="Constants" intro="Here is a list of constants:"/>
<tab type="pages" visible="yes" title="" intro=""/>
<tab type="modules" visible="yes" title="" intro="">
<tab type="modulelist" visible="yes" title="" intro=""/>
<tab type="modulemembers" visible="yes" title="" intro=""/>
</tab>
<tab type="namespaces" visible="yes" title="">
<tab type="namespacelist" visible="yes" title="" intro=""/>
<tab type="namespacemembers" visible="yes" title="" intro=""/>
</tab>
<tab type="concepts" visible="yes" title="">
</tab>
<tab type="interfaces" visible="yes" title="">
<tab type="interfacelist" visible="yes" title="" intro=""/>
<tab type="interfaceindex" visible="$ALPHABETICAL_INDEX" title=""/>
@@ -35,4 +42,228 @@
</tab>
<tab type="examples" visible="yes" title="" intro=""/>
</navindex>
<!-- Layout definition for a class page -->
<class>
<briefdescription visible="yes"/>
<includes visible="$SHOW_HEADERFILE"/>
<inheritancegraph visible="yes"/>
<collaborationgraph visible="yes"/>
<memberdecl>
<nestedclasses visible="yes" title=""/>
<publictypes visible="yes" title=""/>
<services visible="yes" title=""/>
<interfaces visible="yes" title=""/>
<publicslots visible="yes" title=""/>
<signals visible="yes" title=""/>
<publicmethods visible="yes" title=""/>
<publicstaticmethods visible="yes" title=""/>
<publicattributes visible="yes" title=""/>
<publicstaticattributes visible="yes" title=""/>
<protectedtypes visible="yes" title=""/>
<protectedslots visible="yes" title=""/>
<protectedmethods visible="yes" title=""/>
<protectedstaticmethods visible="yes" title=""/>
<protectedattributes visible="yes" title=""/>
<protectedstaticattributes visible="yes" title=""/>
<packagetypes visible="yes" title=""/>
<packagemethods visible="yes" title=""/>
<packagestaticmethods visible="yes" title=""/>
<packageattributes visible="yes" title=""/>
<packagestaticattributes visible="yes" title=""/>
<properties visible="yes" title=""/>
<events visible="yes" title=""/>
<privatetypes visible="yes" title=""/>
<privateslots visible="yes" title=""/>
<privatemethods visible="yes" title=""/>
<privatestaticmethods visible="yes" title=""/>
<privateattributes visible="yes" title=""/>
<privatestaticattributes visible="yes" title=""/>
<friends visible="yes" title=""/>
<related visible="yes" title="" subtitle=""/>
<membergroups visible="yes"/>
</memberdecl>
<detaileddescription visible="yes" title=""/>
<memberdef>
<inlineclasses visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<enums visible="yes" title=""/>
<services visible="yes" title=""/>
<interfaces visible="yes" title=""/>
<constructors visible="yes" title=""/>
<functions visible="yes" title=""/>
<related visible="yes" title=""/>
<variables visible="yes" title=""/>
<properties visible="yes" title=""/>
<events visible="yes" title=""/>
</memberdef>
<allmemberslink visible="yes"/>
<usedfiles visible="$SHOW_USED_FILES"/>
<authorsection visible="yes"/>
</class>
<!-- Layout definition for a namespace page -->
<namespace>
<briefdescription visible="yes"/>
<memberdecl>
<nestednamespaces visible="yes" title=""/>
<constantgroups visible="yes" title=""/>
<interfaces visible="yes" title=""/>
<classes visible="yes" title=""/>
<concepts visible="yes" title=""/>
<structs visible="yes" title=""/>
<exceptions visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<sequences visible="yes" title=""/>
<dictionaries visible="yes" title=""/>
<enums visible="yes" title=""/>
<functions visible="yes" title=""/>
<variables visible="yes" title=""/>
<properties visible="yes" title=""/>
<membergroups visible="yes" visible="yes"/>
</memberdecl>
<detaileddescription visible="yes" title=""/>
<memberdef>
<inlineclasses visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<sequences visible="yes" title=""/>
<dictionaries visible="yes" title=""/>
<enums visible="yes" title=""/>
<functions visible="yes" title=""/>
<variables visible="yes" title=""/>
<properties visible="yes" title=""/>
</memberdef>
<authorsection visible="yes"/>
</namespace>
<!-- Layout definition for a concept page -->
<concept>
<briefdescription visible="yes"/>
<includes visible="$SHOW_HEADERFILE"/>
<definition visible="yes" title=""/>
<detaileddescription visible="yes" title=""/>
<authorsection visible="yes"/>
</concept>
<!-- Layout definition for a file page -->
<file>
<briefdescription visible="yes"/>
<includes visible="$SHOW_INCLUDE_FILES"/>
<includegraph visible="yes"/>
<includedbygraph visible="yes"/>
<sourcelink visible="yes"/>
<memberdecl>
<interfaces visible="yes" title=""/>
<classes visible="yes" title=""/>
<structs visible="yes" title=""/>
<exceptions visible="yes" title=""/>
<namespaces visible="yes" title=""/>
<concepts visible="yes" title=""/>
<constantgroups visible="yes" title=""/>
<defines visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<sequences visible="yes" title=""/>
<dictionaries visible="yes" title=""/>
<enums visible="yes" title=""/>
<functions visible="yes" title=""/>
<variables visible="yes" title=""/>
<properties visible="yes" title=""/>
<membergroups visible="yes" visible="yes"/>
</memberdecl>
<detaileddescription visible="yes" title=""/>
<memberdef>
<inlineclasses visible="yes" title=""/>
<defines visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<sequences visible="yes" title=""/>
<dictionaries visible="yes" title=""/>
<enums visible="yes" title=""/>
<functions visible="yes" title=""/>
<variables visible="yes" title=""/>
<properties visible="yes" title=""/>
</memberdef>
<authorsection/>
</file>
<!-- Layout definition for a group page -->
<group>
<briefdescription visible="yes"/>
<groupgraph visible="yes"/>
<memberdecl>
<nestedgroups visible="yes" title=""/>
<modules visible="yes" title=""/>
<dirs visible="yes" title=""/>
<files visible="yes" title=""/>
<namespaces visible="yes" title=""/>
<concepts visible="yes" title=""/>
<classes visible="yes" title=""/>
<defines visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<sequences visible="yes" title=""/>
<dictionaries visible="yes" title=""/>
<enums visible="yes" title=""/>
<enumvalues visible="yes" title=""/>
<functions visible="yes" title=""/>
<variables visible="yes" title=""/>
<signals visible="yes" title=""/>
<publicslots visible="yes" title=""/>
<protectedslots visible="yes" title=""/>
<privateslots visible="yes" title=""/>
<events visible="yes" title=""/>
<properties visible="yes" title=""/>
<friends visible="yes" title=""/>
<membergroups visible="yes"/>
</memberdecl>
<detaileddescription visible="yes" title=""/>
<memberdef>
<pagedocs/>
<inlineclasses visible="yes" title=""/>
<defines visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<sequences visible="yes" title=""/>
<dictionaries visible="yes" title=""/>
<enums visible="yes" title=""/>
<enumvalues visible="yes" title=""/>
<functions visible="yes" title=""/>
<variables visible="yes" title=""/>
<signals visible="yes" title=""/>
<publicslots visible="yes" title=""/>
<protectedslots visible="yes" title=""/>
<privateslots visible="yes" title=""/>
<events visible="yes" title=""/>
<properties visible="yes" title=""/>
<friends visible="yes" title=""/>
</memberdef>
<authorsection visible="yes"/>
</group>
<!-- Layout definition for a C++20 module page -->
<module>
<briefdescription visible="yes"/>
<exportedmodules visible="yes"/>
<memberdecl>
<concepts visible="yes" title=""/>
<classes visible="yes" title=""/>
<enums visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<functions visible="yes" title=""/>
<variables visible="yes" title=""/>
<membergroups visible="yes" title=""/>
</memberdecl>
<detaileddescription visible="yes" title=""/>
<memberdecl>
<files visible="yes"/>
</memberdecl>
</module>
<!-- Layout definition for a directory page -->
<directory>
<briefdescription visible="yes"/>
<directorygraph visible="yes"/>
<memberdecl>
<dirs visible="yes"/>
<files visible="yes"/>
</memberdecl>
<detaileddescription visible="yes" title=""/>
</directory>
</doxygenlayout>

View File

@@ -2132,7 +2132,10 @@ uint32_t dc_lookup_contact_id_by_addr (dc_context_t* context, const char*
uint32_t dc_create_contact (dc_context_t* context, const char* name, const char* addr);
#define DC_GCL_VERIFIED_ONLY 0x01
// Deprecated 2025-05-20, setting this flag is a no-op.
#define DC_GCL_DEPRECATED_VERIFIED_ONLY 0x01
#define DC_GCL_ADD_SELF 0x02
@@ -2194,8 +2197,6 @@ dc_array_t* dc_import_vcard (dc_context_t* context, const char*
* @param context The context object.
* @param flags A combination of flags:
* - if the flag DC_GCL_ADD_SELF is set, SELF is added to the list unless filtered by other parameters
* - if the flag DC_GCL_VERIFIED_ONLY is set, only verified contacts are returned.
* if DC_GCL_VERIFIED_ONLY is not set, verified and unverified contacts are returned.
* @param query A string to filter the list. Typically used to implement an
* incremental search. NULL for no filtering.
* @return An array containing all contact IDs. Must be dc_array_unref()'d

View File

@@ -8,6 +8,7 @@ use deltachat::chat::ChatVisibility;
use deltachat::contact::Contact;
use deltachat::context::Context;
use deltachat::download;
use deltachat::log::LogExt as _;
use deltachat::message::Message;
use deltachat::message::MsgId;
use deltachat::message::Viewtype;
@@ -23,6 +24,7 @@ use super::webxdc::WebxdcMessageInfo;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase", tag = "kind")]
#[expect(clippy::large_enum_variant)]
pub enum MessageLoadResult {
Message(MessageObject),
LoadingError { error: String },
@@ -143,7 +145,10 @@ impl MessageObject {
let override_sender_name = message.get_override_sender_name();
let webxdc_info = if message.get_viewtype() == Viewtype::Webxdc {
Some(WebxdcMessageInfo::get_for_message(context, msg_id).await?)
WebxdcMessageInfo::get_for_message(context, msg_id)
.await
.log_err(context)
.ok()
} else {
None
};

View File

@@ -2,31 +2,31 @@
"author": "Delta Chat Developers (ML) <delta@codespeak.net>",
"dependencies": {
"@deltachat/tiny-emitter": "3.0.0",
"isomorphic-ws": "^4.0.1",
"isomorphic-ws": "^5.0.0",
"yerpc": "^0.6.2"
},
"devDependencies": {
"@types/chai": "^4.2.21",
"@types/chai-as-promised": "^7.1.5",
"@types/mocha": "^9.0.0",
"@types/ws": "^7.2.4",
"c8": "^7.10.0",
"@types/chai": "^4.3.10",
"@types/chai-as-promised": "^7.1.8",
"@types/mocha": "^10.0.4",
"@types/ws": "^8.5.9",
"c8": "^8.0.1",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"esbuild": "^0.17.9",
"esbuild": "^0.25.5",
"http-server": "^14.1.1",
"mocha": "^9.1.1",
"mocha": "^10.2.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.6.2",
"typedoc": "^0.23.2",
"typescript": "^4.5.5",
"prettier": "^3.5.3",
"typedoc": "^0.28.5",
"typescript": "^5.8.3",
"ws": "^8.5.0"
},
"exports": {
".": {
"types": "./dist/deltachat.d.ts",
"import": "./dist/deltachat.js",
"require": "./dist/deltachat.cjs",
"types": "./dist/deltachat.d.ts"
"require": "./dist/deltachat.cjs"
}
},
"license": "MPL-2.0",

View File

@@ -5,24 +5,24 @@ const json = JSON.parse(readFileSync("./coverage/coverage-final.json"));
const jsonCoverage =
json[Object.keys(json).find((k) => k.includes(generatedFile))];
const fnMap = Object.keys(jsonCoverage.fnMap).map(
(key) => jsonCoverage.fnMap[key]
(key) => jsonCoverage.fnMap[key],
);
const htmlCoverage = readFileSync(
"./coverage/" + generatedFile + ".html",
"utf8"
"utf8",
);
const uncoveredLines = htmlCoverage
.split("\n")
.filter((line) => line.includes(`"function not covered"`));
const uncoveredFunctions = uncoveredLines.map(
(line) => />([\w_]+)\(/.exec(line)[1]
(line) => />([\w_]+)\(/.exec(line)[1],
);
console.log(
"\nUncovered api functions:\n" +
uncoveredFunctions
.map((uF) => fnMap.find(({ name }) => name === uF))
.map(
({ name, line }) => `.${name.padEnd(40)} (${generatedFile}:${line})`
({ name, line }) => `.${name.padEnd(40)} (${generatedFile}:${line})`,
)
.join("\n")
.join("\n"),
);

View File

@@ -24,7 +24,7 @@ while (null != (match = regex.exec(header_data))) {
const constants = data
.filter(
({ key }) => key.toUpperCase()[0] === key[0] // check if define name is uppercase
({ key }) => key.toUpperCase()[0] === key[0], // check if define name is uppercase
)
.sort((lhs, rhs) => {
if (lhs.key < rhs.key) return -1;
@@ -50,5 +50,5 @@ const constants = data
writeFileSync(
resolve(__dirname, "../generated/constants.ts"),
`// Generated!\n\nexport enum C {\n${constants.replace(/:/g, " =")},\n}\n`
`// Generated!\n\nexport enum C {\n${constants.replace(/:/g, " =")},\n}\n`,
);

View File

@@ -8,13 +8,13 @@ import { TinyEmitter } from "@deltachat/tiny-emitter";
type Events = { ALL: (accountId: number, event: EventType) => void } & {
[Property in EventType["kind"]]: (
accountId: number,
event: Extract<EventType, { kind: Property }>
event: Extract<EventType, { kind: Property }>,
) => void;
};
type ContextEvents = { ALL: (event: EventType) => void } & {
[Property in EventType["kind"]]: (
event: Extract<EventType, { kind: Property }>
event: Extract<EventType, { kind: Property }>,
) => void;
};
@@ -25,7 +25,7 @@ export type DcEventType<T extends EventType["kind"]> = Extract<
>;
export class BaseDeltaChat<
Transport extends BaseTransport<any>
Transport extends BaseTransport<any>,
> extends TinyEmitter<Events> {
rpc: RawClient;
account?: T.Account;
@@ -34,7 +34,10 @@ export class BaseDeltaChat<
//@ts-ignore
private eventTask: Promise<void>;
constructor(public transport: Transport, startEventLoop: boolean) {
constructor(
public transport: Transport,
startEventLoop: boolean,
) {
super();
this.rpc = new RawClient(this.transport);
if (startEventLoop) {
@@ -53,7 +56,7 @@ export class BaseDeltaChat<
this.contextEmitters[event.contextId].emit(
event.event.kind,
//@ts-ignore
event.event as any
event.event as any,
);
this.contextEmitters[event.contextId].emit("ALL", event.event as any);
}
@@ -83,7 +86,10 @@ export class StdioDeltaChat extends BaseDeltaChat<StdioTransport> {
}
export class StdioTransport extends BaseTransport {
constructor(public input: any, public output: any) {
constructor(
public input: any,
public output: any,
) {
super();
var buffer = "";

View File

@@ -1,4 +1,3 @@
import { strictEqual } from "assert";
import chai, { assert, expect } from "chai";
import chaiAsPromised from "chai-as-promised";
chai.use(chaiAsPromised);
@@ -32,14 +31,14 @@ describe("basic tests", () => {
expect(
await Promise.all(
validAddresses.map((email) => dc.rpc.checkEmailValidity(email))
)
validAddresses.map((email) => dc.rpc.checkEmailValidity(email)),
),
).to.not.contain(false);
expect(
await Promise.all(
invalidAddresses.map((email) => dc.rpc.checkEmailValidity(email))
)
invalidAddresses.map((email) => dc.rpc.checkEmailValidity(email)),
),
).to.not.contain(true);
});
@@ -85,7 +84,7 @@ describe("basic tests", () => {
const contactId = await dc.rpc.createContact(
accountId,
"example@delta.chat",
null
null,
);
expect((await dc.rpc.getContact(accountId, contactId)).isBlocked).to.be
.false;
@@ -127,7 +126,7 @@ describe("basic tests", () => {
await dc.rpc.batchSetConfig(accountId, config);
const retrieved = await dc.rpc.batchGetConfig(
accountId,
Object.keys(config)
Object.keys(config),
);
expect(retrieved).to.deep.equal(config);
});
@@ -139,7 +138,7 @@ describe("basic tests", () => {
await dc.rpc.batchSetConfig(accountId, config);
const retrieved = await dc.rpc.batchGetConfig(
accountId,
Object.keys(config)
Object.keys(config),
);
expect(retrieved).to.deep.equal(config);
});
@@ -153,7 +152,7 @@ describe("basic tests", () => {
await dc.rpc.batchSetConfig(accountId, config);
const retrieved = await dc.rpc.batchGetConfig(
accountId,
Object.keys(config)
Object.keys(config),
);
expect(retrieved).to.deep.equal(config);
});

View File

@@ -17,12 +17,12 @@ describe("online tests", function () {
if (process.env.COVERAGE && !process.env.COVERAGE_OFFLINE) {
console.error(
"CAN NOT RUN COVERAGE correctly: Missing CHATMAIL_DOMAIN environment variable!\n\n",
"You can set COVERAGE_OFFLINE=1 to circumvent this check and skip the online tests, but those coverage results will be wrong, because some functions can only be tested in the online test"
"You can set COVERAGE_OFFLINE=1 to circumvent this check and skip the online tests, but those coverage results will be wrong, because some functions can only be tested in the online test",
);
process.exit(1);
}
console.log(
"Missing CHATMAIL_DOMAIN environment variable!, skip integration tests"
"Missing CHATMAIL_DOMAIN environment variable!, skip integration tests",
);
this.skip();
}
@@ -36,7 +36,7 @@ describe("online tests", function () {
account1 = createTempUser(process.env.CHATMAIL_DOMAIN);
if (!account1 || !account1.email || !account1.password) {
console.log(
"We didn't got back an account from the api, skip integration tests"
"We didn't got back an account from the api, skip integration tests",
);
this.skip();
}
@@ -44,7 +44,7 @@ describe("online tests", function () {
account2 = createTempUser(process.env.CHATMAIL_DOMAIN);
if (!account2 || !account2.email || !account2.password) {
console.log(
"We didn't got back an account2 from the api, skip integration tests"
"We didn't got back an account2 from the api, skip integration tests",
);
this.skip();
}
@@ -92,7 +92,7 @@ describe("online tests", function () {
accountId2,
chatIdOnAccountB,
false,
false
false,
);
expect(messageList).have.length(1);
@@ -124,11 +124,11 @@ describe("online tests", function () {
accountId2,
chatIdOnAccountB,
false,
false
false,
);
const message = await dc.rpc.getMessage(
accountId2,
messageList.reverse()[0]
messageList.reverse()[0],
);
expect(message.text).equal("Hello2");
// Send message back from B to A
@@ -150,7 +150,7 @@ describe("online tests", function () {
const info = await dc.rpc.getProviderInfo(acc, "example.com");
expect(info).to.be.not.null;
expect(info?.overviewPage).to.equal(
"https://providers.delta.chat/example-com"
"https://providers.delta.chat/example-com",
);
expect(info?.status).to.equal(3);
});
@@ -167,12 +167,12 @@ async function waitForEvent<T extends DcEvent["kind"]>(
dc: DeltaChat,
eventType: T,
accountId: number,
timeout: number = EVENT_TIMEOUT
timeout: number = EVENT_TIMEOUT,
): Promise<Extract<DcEvent, { kind: T }>> {
return new Promise((resolve, reject) => {
const rejectTimeout = setTimeout(
() => reject(new Error("Timeout reached before event came in")),
timeout
timeout,
);
const callback = (contextId: number, event: DcEvent) => {
if (contextId == accountId) {

View File

@@ -14,7 +14,7 @@ export async function startServer(): Promise<RpcServerHandle> {
const tmpDir = await mkdtemp(join(tmpdir(), "deltachat-jsonrpc-test"));
const pathToServerBinary = resolve(
join(await getTargetDir(), "debug/deltachat-rpc-server")
join(await getTargetDir(), "debug/deltachat-rpc-server"),
);
const server = spawn(pathToServerBinary, {
@@ -29,7 +29,7 @@ export async function startServer(): Promise<RpcServerHandle> {
throw new Error(
"Failed to start server executable " +
pathToServerBinary +
", make sure you built it first."
", make sure you built it first.",
);
});
let shouldClose = false;
@@ -83,7 +83,7 @@ function getTargetDir(): Promise<string> {
reject(error);
}
}
}
},
);
});
}

View File

@@ -13,7 +13,7 @@ log = { workspace = true }
nu-ansi-term = { workspace = true }
qr2term = "0.3.3"
rusqlite = { workspace = true }
rustyline = "15"
rustyline = "16"
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
tracing-subscriber = { workspace = true, features = ["env-filter"] }

View File

@@ -1162,17 +1162,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let reaction = arg2;
send_reaction(&context, msg_id, reaction).await?;
}
"listcontacts" | "contacts" | "listverified" => {
let contacts = Contact::get_all(
&context,
if arg0 == "listverified" {
DC_GCL_VERIFIED_ONLY | DC_GCL_ADD_SELF
} else {
DC_GCL_ADD_SELF
},
Some(arg1),
)
.await?;
"listcontacts" | "contacts" => {
let contacts = Contact::get_all(&context, DC_GCL_ADD_SELF, Some(arg1)).await?;
log_contactlist(&context, &contacts).await?;
println!("{} contacts.", contacts.len());
}

View File

@@ -211,8 +211,8 @@ class Account:
def get_contacts(
self,
query: Optional[str] = None,
*,
with_self: bool = False,
verified_only: bool = False,
snapshot: bool = False,
) -> Union[list[Contact], list[AttrDict]]:
"""Get a filtered list of contacts.
@@ -220,12 +220,9 @@ class Account:
:param query: if a string is specified, only return contacts
whose name or e-mail matches query.
:param with_self: if True the self-contact is also included if it matches the query.
:param only_verified: if True only return verified contacts.
:param snapshot: If True return a list of contact snapshots instead of Contact instances.
"""
flags = 0
if verified_only:
flags |= ContactFlag.VERIFIED_ONLY
if with_self:
flags |= ContactFlag.ADD_SELF

View File

@@ -8,7 +8,6 @@ COMMAND_PREFIX = "/"
class ContactFlag(IntEnum):
"""Bit flags for get_contacts() method."""
VERIFIED_ONLY = 0x01
ADD_SELF = 0x02

View File

@@ -22,6 +22,8 @@ ignore = [
skip = [
{ name = "async-channel", version = "1.9.0" },
{ name = "bitflags", version = "1.3.2" },
{ name = "derive_more-impl", version = "1.0.0" },
{ name = "derive_more", version = "1.0.0" },
{ name = "event-listener", version = "2.5.3" },
{ name = "generator", version = "0.7.5" },
{ name = "getrandom", version = "0.2.12" },
@@ -35,9 +37,12 @@ skip = [
{ name = "rand_core", version = "0.6.4" },
{ name = "rand", version = "0.8.5" },
{ name = "redox_syscall", version = "0.3.5" },
{ name = "redox_syscall", version = "0.4.1" },
{ name = "regex-automata", version = "0.1.10" },
{ name = "regex-syntax", version = "0.6.29" },
{ name = "rustix", version = "0.38.44" },
{ name = "serdect", version = "0.2.0" },
{ name = "spin", version = "0.9.8" },
{ name = "strum_macros", version = "0.26.2" },
{ name = "strum", version = "0.26.2" },
{ name = "syn", version = "1.0.109" },

24
flake.lock generated
View File

@@ -47,11 +47,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1737527504,
"narHash": "sha256-Z8S5gLPdIYeKwBXDaSxlJ72ZmiilYhu3418h3RSQZA0=",
"lastModified": 1747291057,
"narHash": "sha256-9Wir6aLJAeJKqdoQUiwfKdBn7SyNXTJGRSscRyVOo2Y=",
"owner": "nix-community",
"repo": "fenix",
"rev": "aa13f23e3e91b95377a693ac655bbc6545ebec0d",
"rev": "76ffc1b7b3ec8078fe01794628b6abff35cbda8f",
"type": "github"
},
"original": {
@@ -147,11 +147,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1737469691,
"narHash": "sha256-nmKOgAU48S41dTPIXAq0AHZSehWUn6ZPrUKijHAMmIk=",
"lastModified": 1747179050,
"narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "9e4d5190a9482a1fb9d18adf0bdb83c6e506eaab",
"rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e",
"type": "github"
},
"original": {
@@ -175,11 +175,11 @@
},
"nixpkgs_4": {
"locked": {
"lastModified": 1731139594,
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
"lastModified": 1747179050,
"narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
"rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e",
"type": "github"
},
"original": {
@@ -202,11 +202,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1737453499,
"narHash": "sha256-fa5AJI9mjFU2oVXqdCq2oA2pripAXbHzkUkewJRQpxA=",
"lastModified": 1746889290,
"narHash": "sha256-h3LQYZgyv2l3U7r+mcsrEOGRldaK0zJFwAAva4hV/6g=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "0b68402d781955d526b80e5d479e9e47addb4075",
"rev": "2bafe9d96c6734aacfd49e115f6cf61e7adc68bc",
"type": "github"
},
"original": {

View File

@@ -6,7 +6,7 @@ edition = "2021"
license = "MPL-2.0"
[dev-dependencies]
bolero = "0.8"
bolero = "0.13.3"
[dependencies]
mailparse = { workspace = true }

View File

@@ -355,20 +355,16 @@ class Account:
self,
query: Optional[str] = None,
with_self: bool = False,
only_verified: bool = False,
) -> List[Contact]:
"""get a (filtered) list of contacts.
:param query: if a string is specified, only return contacts
whose name or e-mail matches query.
:param only_verified: if true only return verified contacts.
:param with_self: if true the self-contact is also returned.
:returns: list of :class:`deltachat.contact.Contact` objects.
"""
flags = 0
query_c = as_dc_charpointer(query)
if only_verified:
flags |= const.DC_GCL_VERIFIED_ONLY
if with_self:
flags |= const.DC_GCL_ADD_SELF
dc_array = ffi.gc(lib.dc_get_contacts(self._dc_context, flags, query_c), lib.dc_array_unref)

View File

@@ -434,9 +434,10 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
- First device of the user downloads "member added" from the group.
- First device removes "member added" from the server.
- Some new messages are sent to the group.
- Second device comes online, receives these new messages. The result is a verified group with unverified members.
- Second device comes online, receives these new messages.
The result is an unverified group with unverified members.
- First device re-gossips Autocrypt keys to the group.
- Now the seconds device has all members verified.
- Now the second device has all members and group verified.
"""
ac1, ac2 = acfactory.get_online_accounts(2)
acfactory.remove_preconfigured_keys()
@@ -474,12 +475,12 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
ac2_offl.start_io()
msg_in = ac2_offl._evtracker.wait_next_incoming_message()
assert not msg_in.is_system_message()
assert msg_in.text.startswith("[The message was sent with non-verified encryption")
assert msg_in.text == "hi"
ac2_offl_ac1_contact = msg_in.get_sender_contact()
assert ac2_offl_ac1_contact.addr == ac1.get_config("addr")
assert not ac2_offl_ac1_contact.is_verified()
chat2_offl = msg_in.chat
assert chat2_offl.is_protected()
assert not chat2_offl.is_protected()
lp.sec("ac2: sending message re-gossiping Autocrypt keys")
chat2.send_text("hi2")
@@ -500,6 +501,7 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
assert msg_in.text == "hi2"
assert msg_in.chat == chat2_offl
assert msg_in.get_sender_contact().addr == ac2.get_config("addr")
assert msg_in.chat.is_protected()
assert ac2_offl_ac1_contact.is_verified()

View File

@@ -1617,7 +1617,7 @@ def test_immediate_autodelete(acfactory, lp):
lp.sec("ac2: wait for close/expunge on autodelete")
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
ac2._evtracker.get_info_contains("close/expunge succeeded")
ac2._evtracker.get_info_contains("Close/expunge succeeded.")
lp.sec("ac2: check that message was autodeleted on server")
assert len(ac2.direct_imap.get_all_messages()) == 0
@@ -1653,7 +1653,7 @@ def test_delete_multiple_messages(acfactory, lp):
lp.sec("ac2: test that only one message is left")
while 1:
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
ac2._evtracker.get_info_contains("close/expunge succeeded")
ac2._evtracker.get_info_contains("Close/expunge succeeded.")
ac2.direct_imap.select_config_folder("inbox")
nr_msgs = len(ac2.direct_imap.get_all_messages())
assert nr_msgs > 0

View File

@@ -184,7 +184,6 @@ class TestOfflineContact:
assert not ac1.get_contacts(query="some2")
assert ac1.get_contacts(query="some1")
assert not ac1.get_contacts(only_verified=True)
assert len(ac1.get_contacts(with_self=True)) == 2
assert ac1.delete_contact(contact1)

View File

@@ -7,7 +7,7 @@ set -euo pipefail
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.86.0
RUST_VERSION=1.87.0
ARCH="$(uname -m)"
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu

View File

@@ -93,7 +93,7 @@ impl<'a> BlobObject<'a> {
if let Some(extension) = original_name.extension().filter(|e| e.len() <= 32) {
let extension = extension.to_string_lossy().to_lowercase();
let extension = sanitize_filename(&extension);
format!("$BLOBDIR/{hash}.{}", extension)
format!("$BLOBDIR/{hash}.{extension}")
} else {
format!("$BLOBDIR/{hash}")
};

View File

@@ -653,7 +653,7 @@ impl ChatId {
) -> Result<()> {
let chat_id = ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Yes)
.await
.with_context(|| format!("can't create chat for {}", contact_id))?;
.with_context(|| format!("can't create chat for {contact_id}"))?;
chat_id
.set_protection(
context,
@@ -2943,7 +2943,10 @@ async fn prepare_send_msg(
if chat.typ != Chattype::Single && !context.get_config_bool(Config::Bot).await? {
if let Some(quoted_message) = msg.quoted_message(context).await? {
if quoted_message.chat_id != chat_id {
bail!("Bad quote reply");
bail!(
"Quote of message from {} cannot be sent to {chat_id}",
quoted_message.chat_id
);
}
}
}
@@ -3534,37 +3537,62 @@ pub async fn get_chat_media(
msg_type2: Viewtype,
msg_type3: Viewtype,
) -> Result<Vec<MsgId>> {
// TODO This query could/should be converted to `AND type IN (?, ?, ?)`.
let list = context
.sql
.query_map(
"SELECT id
let list = if msg_type == Viewtype::Webxdc
&& msg_type2 == Viewtype::Unknown
&& msg_type3 == Viewtype::Unknown
{
context
.sql
.query_map(
"SELECT id
FROM msgs
WHERE (1=? OR chat_id=?)
AND chat_id != ?
AND (type=? OR type=? OR type=?)
AND type = ?
AND hidden=0
ORDER BY max(timestamp, timestamp_rcvd), id;",
(
chat_id.is_none(),
chat_id.unwrap_or_else(|| ChatId::new(0)),
DC_CHAT_ID_TRASH,
Viewtype::Webxdc,
),
|row| row.get::<_, MsgId>(0),
|ids| Ok(ids.flatten().collect()),
)
.await?
} else {
context
.sql
.query_map(
"SELECT id
FROM msgs
WHERE (1=? OR chat_id=?)
AND chat_id != ?
AND type IN (?, ?, ?)
AND hidden=0
ORDER BY timestamp, id;",
(
chat_id.is_none(),
chat_id.unwrap_or_else(|| ChatId::new(0)),
DC_CHAT_ID_TRASH,
msg_type,
if msg_type2 != Viewtype::Unknown {
msg_type2
} else {
msg_type
},
if msg_type3 != Viewtype::Unknown {
msg_type3
} else {
msg_type
},
),
|row| row.get::<_, MsgId>(0),
|ids| Ok(ids.flatten().collect()),
)
.await?;
(
chat_id.is_none(),
chat_id.unwrap_or_else(|| ChatId::new(0)),
DC_CHAT_ID_TRASH,
msg_type,
if msg_type2 != Viewtype::Unknown {
msg_type2
} else {
msg_type
},
if msg_type3 != Viewtype::Unknown {
msg_type3
} else {
msg_type
},
),
|row| row.get::<_, MsgId>(0),
|ids| Ok(ids.flatten().collect()),
)
.await?
};
Ok(list)
}
@@ -4315,7 +4343,7 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
.sql
.query_get_value("SELECT timestamp FROM msgs WHERE id=?", (id,))
.await?
.context("No message {id}")?;
.with_context(|| format!("No message {id}"))?;
msgs.push((ts, *id));
}
msgs.sort_unstable();
@@ -4370,7 +4398,17 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
/// Save a copy of the message in "Saved Messages"
/// and send a sync messages so that other devices save the message as well, unless deleted there.
pub async fn save_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
for src_msg_id in msg_ids {
let mut msgs = Vec::with_capacity(msg_ids.len());
for id in msg_ids {
let ts: i64 = context
.sql
.query_get_value("SELECT timestamp FROM msgs WHERE id=?", (id,))
.await?
.with_context(|| format!("No message {id}"))?;
msgs.push((ts, *id));
}
msgs.sort_unstable();
for (_, src_msg_id) in msgs {
let dest_rfc724_mid = create_outgoing_rfc724_mid();
let src_rfc724_mid = save_copy_in_self_talk(context, src_msg_id, &dest_rfc724_mid).await?;
context
@@ -4391,11 +4429,11 @@ pub async fn save_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
/// Returns data needed to add a `SaveMessage` sync item.
pub(crate) async fn save_copy_in_self_talk(
context: &Context,
src_msg_id: &MsgId,
src_msg_id: MsgId,
dest_rfc724_mid: &String,
) -> Result<String> {
let dest_chat_id = ChatId::create_for_contact(context, ContactId::SELF).await?;
let mut msg = Message::load_from_db(context, *src_msg_id).await?;
let mut msg = Message::load_from_db(context, src_msg_id).await?;
msg.param.remove(Param::Cmd);
msg.param.remove(Param::WebxdcDocument);
msg.param.remove(Param::WebxdcDocumentTimestamp);
@@ -4433,7 +4471,7 @@ pub(crate) async fn save_copy_in_self_talk(
.await?;
let dest_msg_id = MsgId::new(row_id.try_into()?);
context.emit_msgs_changed(msg.chat_id, *src_msg_id);
context.emit_msgs_changed(msg.chat_id, src_msg_id);
context.emit_msgs_changed(dest_chat_id, dest_msg_id);
chatlist_events::emit_chatlist_changed(context);
chatlist_events::emit_chatlist_item_changed(context, dest_chat_id);

View File

@@ -2286,6 +2286,40 @@ async fn test_save_msgs() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_save_msgs_order() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let alice1 = &tcm.alice().await;
for a in [alice, alice1] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
let chat_id = create_group_chat(alice, ProtectionStatus::Protected, "grp").await?;
let sent = [
alice.send_text(chat_id, "0").await,
alice.send_text(chat_id, "1").await,
];
// NB: This doesn't work if sync messages are fetched earlier than the referenced ones.
for msg in &sent {
alice1.recv_msg(msg).await;
}
save_msgs(alice, &[sent[1].sender_msg_id, sent[0].sender_msg_id]).await?;
sync(alice, alice1).await;
for a in [alice, alice1] {
let self_chat = a.get_self_chat().await;
let msgs = get_chat_msgs(a, self_chat.id).await?;
for i in [0, 1] {
let ChatItem::Message { msg_id } = msgs[msgs.len() - 2 + i] else {
panic!("Wrong item type");
};
let msg = Message::load_from_db(a, msg_id).await.unwrap();
assert_eq!(msg.get_text(), format!("{i}"));
}
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_saved_msgs_not_added_to_shared_chats() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -2924,6 +2958,61 @@ async fn test_get_chat_media() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_chat_media_webxdc_order() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let chat = alice.create_chat(&bob).await;
let mut instance1 = Message::new(Viewtype::Webxdc);
instance1.set_file_from_bytes(
&alice,
"test1.xdc",
include_bytes!("../../test-data/webxdc/minimal.xdc"),
None,
)?;
let instance1_id = send_msg(&alice, chat.id, &mut instance1).await?;
let mut instance2 = Message::new(Viewtype::Webxdc);
instance2.set_file_from_bytes(
&alice,
"test2.xdc",
include_bytes!("../../test-data/webxdc/minimal.xdc"),
None,
)?;
let instance2_id = send_msg(&alice, chat.id, &mut instance2).await?;
// list is ordered oldest to newest, check that
let media = get_chat_media(
&alice,
Some(chat.id),
Viewtype::Webxdc,
Viewtype::Unknown,
Viewtype::Unknown,
)
.await?;
assert_eq!(media.first().unwrap(), &instance1_id);
assert_eq!(media.get(1).unwrap(), &instance2_id);
// add a status update for the oder instance; that resorts the list
alice
.send_webxdc_status_update(instance1_id, r#"{"payload": {"foo": "bar"}}"#)
.await?;
let media = get_chat_media(
&alice,
Some(chat.id),
Viewtype::Webxdc,
Viewtype::Unknown,
Viewtype::Unknown,
)
.await?;
assert_eq!(media.first().unwrap(), &instance2_id);
assert_eq!(media.get(1).unwrap(), &instance1_id);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_blob_renaming() -> Result<()> {
let alice = TestContext::new_alice().await;
@@ -3868,8 +3957,10 @@ async fn test_restore_backup_after_60_days() -> Result<()> {
alice.recv_msg(&bob_sent_text).await;
fiona.recv_msg(&bob_sent_text).await;
// Alice did not knew that Charlie is not part of the group
// when sending a message, so sent it to Charlie.
bob.recv_msg(&alice_sent_text).await;
fiona.recv_msg(&alice_sent_text).await;
charlie.recv_msg(&alice_sent_text).await;
// Alice should have learned about Charlie not being part of the group
// by receiving Bob's message.

View File

@@ -322,7 +322,7 @@ impl Chatlist {
(chat_id, MessageState::OutDraft),
)
.await
.with_context(|| format!("failed to get msg ID for chat {}", chat_id))?;
.with_context(|| format!("failed to get msg ID for chat {chat_id}"))?;
ids.push((chat_id, msg_id));
}
Ok(Chatlist { ids })

View File

@@ -88,7 +88,6 @@ pub const DC_GCL_NO_SPECIALS: usize = 0x02;
pub const DC_GCL_ADD_ALLDONE_HINT: usize = 0x04;
pub const DC_GCL_FOR_FORWARDING: usize = 0x08;
pub const DC_GCL_VERIFIED_ONLY: u32 = 0x01;
pub const DC_GCL_ADD_SELF: u32 = 0x02;
// unchanged user avatars are resent to the recipients every some days

View File

@@ -25,7 +25,7 @@ use crate::blob::BlobObject;
use crate::chat::{ChatId, ChatIdBlocked, ProtectionStatus};
use crate::color::str_to_color;
use crate::config::Config;
use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY};
use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF};
use crate::context::Context;
use crate::events::EventType;
use crate::key::{load_self_public_key, DcKey, SignedPublicKey};
@@ -287,6 +287,7 @@ pub async fn make_vcard(context: &Context, contacts: &[ContactId]) -> Result<Str
authname: c.authname,
key,
profile_image,
biography: Some(c.status).filter(|s| !s.is_empty()),
// Use the current time to not reveal our or contact's online time.
timestamp: Ok(now),
});
@@ -423,6 +424,14 @@ async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Resu
);
}
}
if let Some(biography) = &contact.biography {
if let Err(e) = set_status(context, id, biography.to_owned(), false, false).await {
warn!(
context,
"import_vcard_contact: Could not set biography for {}: {e:#}.", contact.addr
);
}
}
Ok(id)
}
@@ -1041,9 +1050,8 @@ impl Contact {
///
/// `listflags` is a combination of flags:
/// - if the flag DC_GCL_ADD_SELF is set, SELF is added to the list unless filtered by other parameters
/// - if the flag DC_GCL_VERIFIED_ONLY is set, only verified contacts are returned.
/// if DC_GCL_VERIFIED_ONLY is not set, verified and unverified contacts are returned.
/// `query` is a string to filter the list.
///
/// `query` is a string to filter the list.
pub async fn get_all(
context: &Context,
listflags: u32,
@@ -1056,14 +1064,13 @@ impl Contact {
.collect::<HashSet<_>>();
let mut add_self = false;
let mut ret = Vec::new();
let flag_verified_only = (listflags & DC_GCL_VERIFIED_ONLY) != 0;
let flag_add_self = (listflags & DC_GCL_ADD_SELF) != 0;
let minimal_origin = if context.get_config_bool(Config::Bot).await? {
Origin::Unknown
} else {
Origin::IncomingReplyTo
};
if flag_verified_only || query.is_some() {
if query.is_some() {
let s3str_like_cmd = format!("%{}%", query.unwrap_or(""));
context
.sql
@@ -1074,14 +1081,12 @@ impl Contact {
AND c.origin>=? \
AND c.blocked=0 \
AND (iif(c.name='',c.authname,c.name) LIKE ? OR c.addr LIKE ?) \
AND (1=? OR LENGTH(ps.verified_key_fingerprint)!=0) \
ORDER BY c.last_seen DESC, c.id DESC;",
(
ContactId::LAST_SPECIAL,
minimal_origin,
&s3str_like_cmd,
&s3str_like_cmd,
if flag_verified_only { 0i32 } else { 1i32 },
),
|row| {
let id: ContactId = row.get(0)?;

View File

@@ -1054,6 +1054,8 @@ async fn test_make_n_import_vcard() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
bob.set_config(Config::Displayname, Some("Bob")).await?;
bob.set_config(Config::Selfstatus, Some("It's me, bob"))
.await?;
let avatar_path = bob.dir.path().join("avatar.png");
let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png");
let avatar_base64 = base64::engine::general_purpose::STANDARD.encode(avatar_bytes);
@@ -1061,6 +1063,7 @@ async fn test_make_n_import_vcard() -> Result<()> {
bob.set_config(Config::Selfavatar, Some(avatar_path.to_str().unwrap()))
.await?;
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
let bob_biography = bob.get_config(Config::Selfstatus).await?.unwrap();
let chat = bob.create_chat(alice).await;
let sent_msg = bob.send_text(chat.id, "moin").await;
alice.recv_msg(&sent_msg).await;
@@ -1086,12 +1089,14 @@ async fn test_make_n_import_vcard() -> Result<()> {
assert_eq!(contacts[0].authname, "Bob".to_string());
assert_eq!(*contacts[0].key.as_ref().unwrap(), key_base64);
assert_eq!(*contacts[0].profile_image.as_ref().unwrap(), avatar_base64);
assert_eq!(*contacts[0].biography.as_ref().unwrap(), bob_biography);
let timestamp = *contacts[0].timestamp.as_ref().unwrap();
assert!(t0 <= timestamp && timestamp <= t1);
assert_eq!(contacts[1].addr, "fiona@example.net".to_string());
assert_eq!(contacts[1].authname, "".to_string());
assert_eq!(contacts[1].key, None);
assert_eq!(contacts[1].profile_image, None);
assert_eq!(contacts[1].biography, None);
let timestamp = *contacts[1].timestamp.as_ref().unwrap();
assert!(t0 <= timestamp && timestamp <= t1);
@@ -1114,6 +1119,7 @@ async fn test_make_n_import_vcard() -> Result<()> {
assert_eq!(contacts[0].authname, "Bob".to_string());
assert_eq!(*contacts[0].key.as_ref().unwrap(), key_base64);
assert_eq!(*contacts[0].profile_image.as_ref().unwrap(), avatar_base64);
assert_eq!(*contacts[0].biography.as_ref().unwrap(), bob_biography);
assert!(contacts[0].timestamp.is_ok());
assert_eq!(contacts[1].addr, "fiona@example.net".to_string());
@@ -1145,6 +1151,7 @@ async fn test_make_n_import_vcard() -> Result<()> {
assert_eq!(contacts[0].authname, "".to_string());
assert_eq!(contacts[0].key, None);
assert_eq!(contacts[0].profile_image, None);
assert_eq!(contacts[0].biography, None);
assert!(contacts[0].timestamp.is_ok());
Ok(())

View File

@@ -4,19 +4,19 @@ use std::collections::{BTreeMap, HashMap};
use std::ffi::OsString;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use anyhow::{bail, ensure, Context as _, Result};
use anyhow::{Context as _, Result, bail, ensure};
use async_channel::{self as channel, Receiver, Sender};
use pgp::composed::SignedPublicKey;
use pgp::types::PublicKeyTrait;
use pgp::SignedPublicKey;
use ratelimit::Ratelimit;
use tokio::sync::{Mutex, Notify, RwLock};
use crate::aheader::EncryptPreference;
use crate::chat::{get_chat_cnt, ChatId, ProtectionStatus};
use crate::chat::{ChatId, ProtectionStatus, get_chat_cnt};
use crate::chatlist_events;
use crate::config::Config;
use crate::constants::{
@@ -27,7 +27,7 @@ use crate::debug_logging::DebugLogging;
use crate::download::DownloadState;
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::imap::{FolderMeaning, Imap, ServerMetadata};
use crate::key::{load_self_public_key, load_self_secret_key, DcKey as _};
use crate::key::{DcKey as _, load_self_public_key, load_self_secret_key};
use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam};
use crate::message::{self, Message, MessageState, MsgId};
use crate::param::{Param, Params};
@@ -35,7 +35,7 @@ use crate::peer_channels::Iroh;
use crate::peerstate::Peerstate;
use crate::push::PushSubscriber;
use crate::quota::QuotaInfo;
use crate::scheduler::{convert_folder_meaning, SchedulerState};
use crate::scheduler::{SchedulerState, convert_folder_meaning};
use crate::sql::Sql;
use crate::stock_str::StockStrings;
use crate::timesmearing::SmearedTimestamp;
@@ -292,7 +292,7 @@ pub struct InnerContext {
pub(crate) push_subscribed: AtomicBool,
/// Iroh for realtime peer channels.
pub(crate) iroh: Arc<RwLock<Option<Iroh>>>,
pub(crate) iroh: Arc<Iroh>,
}
/// The state of ongoing process.
@@ -450,7 +450,7 @@ impl Context {
debug_logging: std::sync::RwLock::new(None),
push_subscriber,
push_subscribed: AtomicBool::new(false),
iroh: Arc::new(RwLock::new(None)),
iroh: Default::default(),
};
let ctx = Context {
@@ -486,19 +486,7 @@ impl Context {
/// Stops the IO scheduler.
pub async fn stop_io(&self) {
self.scheduler.stop(self).await;
if let Some(iroh) = self.iroh.write().await.take() {
// Close all QUIC connections.
// Spawn into a separate task,
// because Iroh calls `wait_idle()` internally
// and it may take time, especially if the network
// has become unavailable.
tokio::spawn(async move {
// We do not log the error because we do not want the task
// to hold the reference to Context.
let _ = tokio::time::timeout(Duration::from_secs(60), iroh.close()).await;
});
}
self.iroh.close();
}
/// Restarts the IO scheduler if it was running before
@@ -509,9 +497,7 @@ impl Context {
/// Indicate that the network likely has come back.
pub async fn maybe_network(&self) {
if let Some(ref iroh) = *self.iroh.read().await {
iroh.network_change().await;
}
self.iroh.network_change().await;
self.scheduler.maybe_network().await;
}
@@ -1061,21 +1047,21 @@ impl Context {
)
.await?
.unwrap_or_default();
res += &format!("num_msgs {}\n", num_msgs);
res += &format!("num_msgs {num_msgs}\n");
let num_chats: u32 = self
.sql
.query_get_value("SELECT COUNT(*) FROM chats WHERE id>9 AND blocked!=1", ())
.await?
.unwrap_or_default();
res += &format!("num_chats {}\n", num_chats);
res += &format!("num_chats {num_chats}\n");
let db_size = tokio::fs::metadata(&self.sql.dbfile).await?.len();
res += &format!("db_size_bytes {}\n", db_size);
res += &format!("db_size_bytes {db_size}\n");
let secret_key = &load_self_secret_key(self).await?.primary_key;
let key_created = secret_key.created_at().timestamp();
res += &format!("key_created {}\n", key_created);
let key_created = secret_key.public_key().created_at().timestamp();
res += &format!("key_created {key_created}\n");
// how many of the chats active in the last months are:
// - protected
@@ -1155,7 +1141,7 @@ impl Context {
id
}
};
res += &format!("self_reporting_id {}", self_reporting_id);
res += &format!("self_reporting_id {self_reporting_id}");
Ok(res)
}

View File

@@ -15,10 +15,10 @@ use crate::pgp;
/// Tries to decrypt a message, but only if it is structured as an Autocrypt message.
///
/// If successful and the message is encrypted, returns decrypted body.
pub fn try_decrypt(
mail: &ParsedMail<'_>,
private_keyring: &[SignedSecretKey],
) -> Result<Option<::pgp::composed::Message>> {
pub fn try_decrypt<'a>(
mail: &'a ParsedMail<'a>,
private_keyring: &'a [SignedSecretKey],
) -> Result<Option<::pgp::composed::Message<'static>>> {
let Some(encrypted_data_part) = get_encrypted_mime(mail) else {
return Ok(None);
};

View File

@@ -143,7 +143,7 @@ impl EncryptHelper {
let cursor = Cursor::new(&mut raw_message);
mail_to_encrypt.clone().write_part(cursor).ok();
let ctext = pgp::pk_encrypt(&raw_message, keyring, Some(sign_key), compress).await?;
let ctext = pgp::pk_encrypt(raw_message, keyring, Some(sign_key), compress).await?;
Ok(ctext)
}
@@ -153,9 +153,8 @@ impl EncryptHelper {
pub async fn sign(self, context: &Context, mail: &MimePart<'static>) -> Result<String> {
let sign_key = load_self_secret_key(context).await?;
let mut buffer = Vec::new();
let cursor = Cursor::new(&mut buffer);
mail.clone().write_part(cursor).ok();
let signature = pgp::pk_calc_signature(&buffer, &sign_key)?;
mail.clone().write_part(&mut buffer)?;
let signature = pgp::pk_calc_signature(buffer, &sign_key)?;
Ok(signature)
}
}

View File

@@ -285,9 +285,7 @@ impl Imap {
Ok(imap)
}
/// Connects or reconnects if needed.
///
/// It is safe to call this function if already connected, actions are performed only as needed.
/// Connects to IMAP server and returns a new IMAP session.
///
/// Calling this function is not enough to perform IMAP operations. Use [`Imap::prepare`]
/// instead if you are going to actually use connection rather than trying connection
@@ -326,7 +324,7 @@ impl Imap {
}
}
info!(context, "Connecting to IMAP server");
info!(context, "Connecting to IMAP server.");
self.connectivity.set_connecting(context).await;
self.conn_last_try = tools::Time::now();
@@ -411,7 +409,7 @@ impl Imap {
lp.user
)));
self.connectivity.set_preparing(context).await;
info!(context, "Successfully logged into IMAP server");
info!(context, "Successfully logged into IMAP server.");
return Ok(session);
}
@@ -459,10 +457,10 @@ impl Imap {
Err(first_error.unwrap_or_else(|| format_err!("No IMAP connection candidates provided")))
}
/// Prepare for IMAP operation.
/// Prepare a new IMAP session.
///
/// Ensure that IMAP client is connected, folders are created and IMAP capabilities are
/// determined.
/// This creates a new IMAP connection and ensures
/// that folders are created and IMAP capabilities are determined.
pub(crate) async fn prepare(&mut self, context: &Context) -> Result<Session> {
let configuring = false;
let mut session = match self.connect(context, configuring).await {
@@ -1048,7 +1046,7 @@ impl Session {
// Expunge folder if needed, e.g. if some jobs have
// deleted messages on the server.
if let Err(err) = self.maybe_close_folder(context).await {
warn!(context, "failed to close folder: {:?}", err);
warn!(context, "Failed to close folder: {err:#}.");
}
Ok(())

View File

@@ -56,8 +56,7 @@ fn test_build_sequence_sets() {
}
let has_number = |(uids, s): &(Vec<u32>, String), number| {
uids.iter().any(|&n| n == number)
&& s.split(',').any(|n| n.parse::<u32>().unwrap() == number)
uids.contains(&number) && s.split(',').any(|n| n.parse::<u32>().unwrap() == number)
};
let numbers: Vec<_> = (2..=500).step_by(2).collect();

View File

@@ -17,16 +17,24 @@ impl Imap {
session: &mut Session,
) -> Result<bool> {
// First of all, debounce to once per minute:
let mut last_scan = context.last_full_folder_scan.lock().await;
if let Some(last_scan) = *last_scan {
let elapsed_secs = time_elapsed(&last_scan).as_secs();
let debounce_secs = context
.get_config_u64(Config::ScanAllFoldersDebounceSecs)
.await?;
{
let mut last_scan = context.last_full_folder_scan.lock().await;
if let Some(last_scan) = *last_scan {
let elapsed_secs = time_elapsed(&last_scan).as_secs();
let debounce_secs = context
.get_config_u64(Config::ScanAllFoldersDebounceSecs)
.await?;
if elapsed_secs < debounce_secs {
return Ok(false);
if elapsed_secs < debounce_secs {
return Ok(false);
}
}
// Update the timestamp before scanning the folders
// to avoid holding the lock for too long.
// This means next scan is delayed even if
// the current one fails.
last_scan.replace(tools::Time::now());
}
info!(context, "Starting full folder scan");
@@ -94,7 +102,6 @@ impl Imap {
}
info!(context, "Found folders: {folder_names:?}.");
last_scan.replace(tools::Time::now());
Ok(true)
}
}

View File

@@ -33,10 +33,10 @@ impl ImapSession {
pub(super) async fn maybe_close_folder(&mut self, context: &Context) -> anyhow::Result<()> {
if let Some(folder) = &self.selected_folder {
if self.selected_folder_needs_expunge {
info!(context, "Expunge messages in \"{}\".", folder);
info!(context, "Expunge messages in {folder:?}.");
self.close().await.context("IMAP close/expunge failed")?;
info!(context, "close/expunge succeeded");
info!(context, "Close/expunge succeeded.");
self.selected_folder = None;
self.selected_folder_needs_expunge = false;
self.new_mail = false;
@@ -73,6 +73,7 @@ impl ImapSession {
match res {
Ok(mailbox) => {
info!(context, "Selected folder {folder:?}.");
self.selected_folder = Some(folder.to_string());
self.selected_mailbox = Some(mailbox);
Ok(NewlySelected::Yes)
@@ -94,7 +95,7 @@ impl ImapSession {
Ok(newly_selected) => Ok(newly_selected),
Err(err) => match err {
Error::NoFolder(..) => {
info!(context, "Failed to select folder {} because it does not exist, trying to create it.", folder);
info!(context, "Failed to select folder {folder:?} because it does not exist, trying to create it.");
let create_res = self.create(folder).await;
if let Err(ref err) = create_res {
info!(context, "Couldn't select folder, then create() failed: {err:#}.");
@@ -129,7 +130,7 @@ impl ImapSession {
let newly_selected = if create {
self.select_or_create_folder(context, folder)
.await
.with_context(|| format!("failed to select or create folder {folder}"))?
.with_context(|| format!("Failed to select or create folder {folder:?}"))?
} else {
match self.select_folder(context, folder).await {
Ok(newly_selected) => newly_selected,
@@ -137,7 +138,7 @@ impl ImapSession {
Error::NoFolder(..) => return Ok(false),
_ => {
return Err(err)
.with_context(|| format!("failed to select folder {folder}"))?
.with_context(|| format!("Failed to select folder {folder:?}"))?
}
},
}
@@ -145,14 +146,14 @@ impl ImapSession {
let mailbox = self
.selected_mailbox
.as_mut()
.with_context(|| format!("No mailbox selected, folder: {folder}"))?;
.with_context(|| format!("No mailbox selected, folder: {folder:?}"))?;
let old_uid_validity = get_uidvalidity(context, folder)
.await
.with_context(|| format!("failed to get old UID validity for folder {folder}"))?;
.with_context(|| format!("Failed to get old UID validity for folder {folder:?}"))?;
let old_uid_next = get_uid_next(context, folder)
.await
.with_context(|| format!("failed to get old UID NEXT for folder {folder}"))?;
.with_context(|| format!("Failed to get old UID NEXT for folder {folder:?}"))?;
let new_uid_validity = mailbox
.uid_validity

View File

@@ -4,7 +4,6 @@ use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use ::pgp::types::PublicKeyTrait;
use anyhow::{bail, ensure, format_err, Context as _, Result};
use futures::TryStreamExt;
use futures_lite::FutureExt;
@@ -32,6 +31,7 @@ use crate::tools::{
mod key_transfer;
mod transfer;
use ::pgp::types::KeyDetails;
pub use key_transfer::{continue_key_transfer, initiate_key_transfer};
pub use transfer::{get_backup, BackupProvider};
@@ -173,7 +173,11 @@ async fn set_self_key(context: &Context, armored: &str) -> Result<()> {
};
key::store_self_keypair(context, &keypair).await?;
info!(context, "stored self key: {:?}", keypair.secret.key_id());
info!(
context,
"stored self key: {:?}",
keypair.secret.public_key().key_id()
);
Ok(())
}

View File

@@ -1,4 +1,6 @@
//! # Key transfer via Autocrypt Setup Message.
use std::io::BufReader;
use rand::{thread_rng, Rng};
use anyhow::{bail, ensure, Result};
@@ -71,7 +73,7 @@ pub async fn continue_key_transfer(
if let Some(filename) = msg.get_file(context) {
let file = open_file_std(context, filename)?;
let sc = normalize_setup_code(setup_code);
let armored_key = decrypt_setup_file(&sc, file).await?;
let armored_key = decrypt_setup_file(&sc, BufReader::new(file)).await?;
set_self_key(context, &armored_key).await?;
context.set_config_bool(Config::BccSelf, true).await?;
@@ -96,7 +98,7 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<St
true => Some(("Autocrypt-Prefer-Encrypt", "mutual")),
};
let private_key_asc = private_key.to_asc(ac_headers);
let encr = pgp::symm_encrypt(passphrase, private_key_asc.as_bytes())
let encr = pgp::symm_encrypt(passphrase, private_key_asc.into_bytes())
.await?
.replace('\n', "\r\n");
@@ -155,7 +157,7 @@ fn create_setup_code(_context: &Context) -> String {
ret
}
async fn decrypt_setup_file<T: std::io::Read + std::io::Seek>(
async fn decrypt_setup_file<T: std::fmt::Debug + std::io::BufRead + Send + 'static>(
passphrase: &str,
file: T,
) -> Result<String> {
@@ -258,11 +260,10 @@ mod tests {
assert!(!base64.is_empty());
let setup_file = S_EM_SETUPFILE.to_string();
let decrypted =
decrypt_setup_file(S_EM_SETUPCODE, std::io::Cursor::new(setup_file.as_bytes()))
.await
.unwrap();
let setup_file = S_EM_SETUPFILE;
let decrypted = decrypt_setup_file(S_EM_SETUPCODE, setup_file.as_bytes())
.await
.unwrap();
let (typ, headers, _base64) = split_armored_data(decrypted.as_bytes()).unwrap();
@@ -278,14 +279,13 @@ mod tests {
/// "Implementations MUST NOT use plaintext in Symmetrically Encrypted Data packets".
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decrypt_plaintext_autocrypt_setup_message() {
let setup_file = S_PLAINTEXT_SETUPFILE.to_string();
let setup_file = S_PLAINTEXT_SETUPFILE;
let incorrect_setupcode = "0000-0000-0000-0000-0000-0000-0000-0000-0000";
assert!(decrypt_setup_file(
incorrect_setupcode,
std::io::Cursor::new(setup_file.as_bytes()),
)
.await
.is_err());
assert!(
decrypt_setup_file(incorrect_setupcode, setup_file.as_bytes(),)
.await
.is_err()
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -10,7 +10,7 @@ use deltachat_contact_tools::EmailAddress;
use pgp::composed::Deserializable;
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
use pgp::ser::Serialize;
use pgp::types::{PublicKeyTrait, SecretKeyTrait};
use pgp::types::{KeyDetails, KeyId, Password};
use rand::thread_rng;
use tokio::runtime::Handle;
@@ -24,7 +24,7 @@ use crate::tools::{self, time_elapsed};
/// This trait is implemented for rPGP's [SignedPublicKey] and
/// [SignedSecretKey] types and makes working with them a little
/// easier in the deltachat world.
pub(crate) trait DcKey: Serialize + Deserializable + PublicKeyTrait + Clone {
pub(crate) trait DcKey: Serialize + Deserializable + Clone {
/// Create a key from some bytes.
fn from_slice(bytes: &[u8]) -> Result<Self> {
let res = <Self as Deserializable>::from_bytes(Cursor::new(bytes));
@@ -78,7 +78,7 @@ pub(crate) trait DcKey: Serialize + Deserializable + PublicKeyTrait + Clone {
let bytes = data.as_bytes();
let res = Self::from_armor_single(Cursor::new(bytes));
let (key, headers) = match res {
Err(pgp::errors::Error::NoMatchingPacket) => match Self::is_private() {
Err(pgp::errors::Error::NoMatchingPacket { .. }) => match Self::is_private() {
true => bail!("No private key packet found"),
false => bail!("No public key packet found"),
},
@@ -123,11 +123,10 @@ pub(crate) trait DcKey: Serialize + Deserializable + PublicKeyTrait + Clone {
fn to_asc(&self, header: Option<(&str, &str)>) -> String;
/// The fingerprint for the key.
fn dc_fingerprint(&self) -> Fingerprint {
PublicKeyTrait::fingerprint(self).into()
}
fn dc_fingerprint(&self) -> Fingerprint;
fn is_private() -> bool;
fn key_id(&self) -> KeyId;
}
pub(crate) async fn load_self_public_key(context: &Context) -> Result<SignedPublicKey> {
@@ -230,6 +229,14 @@ impl DcKey for SignedPublicKey {
fn is_private() -> bool {
false
}
fn dc_fingerprint(&self) -> Fingerprint {
self.fingerprint().into()
}
fn key_id(&self) -> KeyId {
KeyDetails::key_id(self)
}
}
impl DcKey for SignedSecretKey {
@@ -249,6 +256,14 @@ impl DcKey for SignedSecretKey {
fn is_private() -> bool {
true
}
fn dc_fingerprint(&self) -> Fingerprint {
self.fingerprint().into()
}
fn key_id(&self) -> KeyId {
KeyDetails::key_id(&**self)
}
}
/// Deltachat extension trait for secret keys.
@@ -262,9 +277,14 @@ pub(crate) trait DcSecretKey {
impl DcSecretKey for SignedSecretKey {
fn split_public_key(&self) -> Result<SignedPublicKey> {
self.verify()?;
let unsigned_pubkey = SecretKeyTrait::public_key(self);
let unsigned_pubkey = self.public_key();
let mut rng = thread_rng();
let signed_pubkey = unsigned_pubkey.sign(&mut rng, self, || "".into())?;
let signed_pubkey = unsigned_pubkey.sign(
&mut rng,
&self.primary_key,
self.primary_key.public_key(),
&Password::empty(),
)?;
Ok(signed_pubkey)
}
}

View File

@@ -44,6 +44,7 @@ use crate::webxdc::StatusUpdateSerial;
pub const RECOMMENDED_FILE_SIZE: u64 = 24 * 1024 * 1024 / 4 * 3;
#[derive(Debug, Clone)]
#[expect(clippy::large_enum_variant)]
pub enum Loaded {
Message {
chat: Chat,

View File

@@ -342,10 +342,10 @@ impl MimeMessage {
let mail_raw; // Memory location for a possible decrypted message.
let decrypted_msg; // Decrypted signed OpenPGP message.
let (mail, encrypted) =
let (mail, is_encrypted) =
match tokio::task::block_in_place(|| try_decrypt(&mail, &private_keyring)) {
Ok(Some(msg)) => {
mail_raw = msg.get_content()?.unwrap_or_default();
Ok(Some(mut msg)) => {
mail_raw = msg.as_data_vec().unwrap_or_default();
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
@@ -434,7 +434,7 @@ impl MimeMessage {
signatures.extend(signatures_detached);
content
});
if let (Ok(mail), true) = (mail, encrypted) {
if let (Ok(mail), true) = (mail, is_encrypted) {
if !signatures.is_empty() {
// Remove unsigned opportunistically protected headers from messages considered
// Autocrypt-encrypted / displayed with padlock.
@@ -529,7 +529,7 @@ impl MimeMessage {
}
}
}
if !encrypted {
if !is_encrypted {
signatures.clear();
}
if let Some(peerstate) = &mut peerstate {
@@ -2380,7 +2380,7 @@ async fn handle_ndn(
let aggregated_error = message
.error
.as_ref()
.map(|err| format!("{}\n\n{}", err, err_msg));
.map(|err| format!("{err}\n\n{err_msg}"));
set_msg_failed(
context,
&mut message,

View File

@@ -261,6 +261,18 @@ async fn fetch_url(context: &Context, original_url: &str) -> Result<Response> {
continue;
}
if !response.status().is_success() {
return Err(anyhow!(
"The server returned a non-successful response code: {}{}",
response.status().as_u16(),
response
.status()
.canonical_reason()
.map(|s| format!(" {s}"))
.unwrap_or("".to_string())
));
}
let content_type = response
.headers()
.get("content-type")

View File

@@ -158,6 +158,7 @@ impl Socks5Config {
/// (except for iroh p2p connections)
/// will be sent.
#[derive(Debug, Clone, PartialEq, Eq)]
#[expect(clippy::large_enum_variant)]
pub enum ProxyConfig {
/// HTTP proxy.
Http(HttpConfig),

View File

@@ -23,60 +23,157 @@
//! (scoped per WebXDC app instance/message-id). The other peers can then join the gossip with `joinRealtimeChannel().setListener()`
//! and `joinRealtimeChannel().send()` just like the other peers.
use anyhow::{anyhow, bail, Context as _, Result};
use anyhow::{Context as _, Result, anyhow, bail};
use data_encoding::BASE32_NOPAD;
use futures_lite::StreamExt;
use iroh::{Endpoint, NodeAddr, NodeId, PublicKey, RelayMode, RelayUrl, SecretKey};
use iroh_gossip::net::{Event, Gossip, GossipEvent, JoinOptions, GOSSIP_ALPN};
use iroh_gossip::net::{Event, GOSSIP_ALPN, Gossip, GossipEvent, JoinOptions};
use iroh_gossip::proto::TopicId;
use parking_lot::Mutex;
use std::collections::{BTreeSet, HashMap};
use std::env;
use tokio::sync::{oneshot, RwLock};
use tokio::sync::{RwLock, RwLockReadGuard, oneshot};
use tokio::task::JoinHandle;
use url::Url;
use crate::EventType;
use crate::chat::send_msg;
use crate::config::Config;
use crate::context::Context;
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::EventType;
/// The length of an ed25519 `PublicKey`, in bytes.
const PUBLIC_KEY_LENGTH: usize = 32;
const PUBLIC_KEY_STUB: &[u8] = "static_string".as_bytes();
/// Store iroh peer channels for the context.
#[derive(Debug)]
#[derive(Debug, Default)]
pub struct Iroh {
/// iroh router needed for iroh peer channels.
pub(crate) router: iroh::protocol::Router,
/// [Gossip] needed for iroh peer channels.
pub(crate) gossip: Gossip,
inner: RwLock<Option<EndpointState>>,
/// Sequence numbers for gossip channels.
pub(crate) sequence_numbers: Mutex<HashMap<TopicId, i32>>,
/// Topics for which an advertisement has already been sent.
pub(crate) iroh_channels: RwLock<HashMap<TopicId, ChannelState>>,
/// Currently used Iroh public key.
///
/// This is attached to every message to work around `iroh_gossip` deduplication.
pub(crate) public_key: PublicKey,
}
/// Iroh endpoint and gossip protocol state.
#[derive(Debug)]
struct EndpointState {
/// Router that runs the iroh Endpoint and hosts the gossip protocol.
router: iroh::protocol::Router,
/// The gossip protocol state and tasks.
gossip: Gossip,
/// Subscribed gossip topics.
channels: HashMap<TopicId, ChannelState>,
/// The public key of the iroh Endpoint.
public_key: PublicKey,
}
impl EndpointState {
fn close(self) {
// Spawn into a separate task, because iroh waits for the UDP sockets to be idle,
// which can take some time.
tokio::spawn(async move {
// We do not log the error because we do not want the task to hold a reference
// to the Context.
tokio::time::timeout(std::time::Duration::from_secs(60), self.router.shutdown())
.await
.ok();
});
}
}
// /// Store iroh peer channels for the context.
// #[derive(Debug)]
// pub struct Iroh {
// /// iroh router needed for iroh peer channels.
// router: iroh::protocol::Router,
// /// [Gossip] needed for iroh peer channels.
// gossip: Gossip,
// /// Sequence numbers for gossip channels.
// sequence_numbers: Mutex<HashMap<TopicId, i32>>,
// /// Topics for which an advertisement has already been sent.
// iroh_channels: RwLock<HashMap<TopicId, ChannelState>>,
// /// Currently used Iroh public key.
// ///
// /// This is attached to every message to work around `iroh_gossip` deduplication.
// public_key: PublicKey,
// }
impl Iroh {
/// Notify the endpoint that the network has changed.
pub(crate) async fn network_change(&self) {
self.router.endpoint().network_change().await
/// Creates an iroh endpoint and gossip if it does not yet exists.
async fn init(&self, ctx: &Context) -> Result<()> {
if self.inner.read().await.is_some() {
return Ok(());
}
info!(ctx, "Initializing peer channels.");
let mut inner = self.inner.write().await;
let secret_key = SecretKey::generate(rand::rngs::OsRng);
let public_key = secret_key.public();
let relay_mode = if let Some(relay_url) = ctx
.metadata
.read()
.await
.as_ref()
.and_then(|conf| conf.iroh_relay.clone())
{
RelayMode::Custom(RelayUrl::from(relay_url).into())
} else {
// FIXME: this should be RelayMode::Disabled instead.
// Currently using default relays because otherwise Rust tests fail.
RelayMode::Default
};
let endpoint = Endpoint::builder()
.tls_x509() // For compatibility with iroh <0.34.0
.secret_key(secret_key)
.alpns(vec![GOSSIP_ALPN.to_vec()])
.relay_mode(relay_mode)
.bind()
.await?;
// create gossip
// Allow messages up to 128 KB in size.
// We set the limit to 128 KiB to account for internal overhead,
// but only guarantee 128 KB of payload to WebXDC developers.
let gossip = Gossip::builder()
.max_message_size(128 * 1024)
.spawn(endpoint.clone())
.await?;
let router = iroh::protocol::Router::builder(endpoint)
.accept(GOSSIP_ALPN, gossip.clone())
.spawn();
*inner = Some(EndpointState {
router,
gossip,
channels: HashMap::new(),
public_key,
});
Ok(())
}
/// Closes the QUIC endpoint.
pub(crate) async fn close(self) -> Result<()> {
self.router.shutdown().await.context("Closing iroh failed")
/// Notify the endpoint that the network has changed.
pub(crate) async fn network_change(&self) {
let iroh = self.inner.read().await;
if let Some(inner) = iroh.as_ref() {
inner.router.endpoint().network_change().await;
}
}
/// Closes the iroh endpoint and router.
pub(crate) async fn close(&self) {
if let Some(iroh) = self.inner.write().await.take() {
iroh.close();
}
}
/// Join a topic and create the subscriber loop for it.
@@ -97,9 +194,8 @@ impl Iroh {
// no other thread can create a second gossip subscription
// after we check that it does not exist and before we create a new one.
// Otherwise we would receive every message twice or more times.
let mut iroh_channels = self.iroh_channels.write().await;
if iroh_channels.contains_key(&topic) {
let inner = self.inner.read().await;
if self.inner.read().await.channels.contains_key(&topic) {
return Ok(None);
}
@@ -484,6 +580,21 @@ pub async fn leave_webxdc_realtime(ctx: &Context, msg_id: MsgId) -> Result<()> {
iroh.leave_realtime(topic).await?;
info!(ctx, "IROH_REALTIME: Left gossip for message {msg_id}");
let can_close = iroh.iroh_channels.read().await.is_empty();
if can_close {
drop(iroh);
// Shut down iroh. Currently iroh does a lot of background work, which will improve
// post iroh 1.0, so shut it down when not needed.
info!(ctx, "XXXX can close");
if let Some(iroh) = ctx.iroh.write().await.take() {
info!(ctx, "Closing iroh Endpoint");
iroh.close()
.await
.map_err(|err| error!(ctx, "Error shutting down iroh: {err:#}"))
.ok();
}
}
Ok(())
}
@@ -553,10 +664,10 @@ async fn subscribe_loop(
mod tests {
use super::*;
use crate::{
EventType,
chat::send_msg,
message::{Message, Viewtype},
test_utils::TestContextManager,
EventType,
};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -728,15 +839,23 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_can_reconnect() {
tokio::time::timeout(tokio::time::Duration::from_secs(10), can_reconnect())
.await
.expect("timed out");
}
async fn can_reconnect() {
let mut tcm = TestContextManager::new();
let alice = &mut tcm.alice().await;
let bob = &mut tcm.bob().await;
assert!(alice
.get_config_bool(Config::WebxdcRealtimeEnabled)
.await
.unwrap());
// Alice sends webxdc to bob
assert!(
alice
.get_config_bool(Config::WebxdcRealtimeEnabled)
.await
.unwrap()
);
eprintln!("Alice sends webxdx to bob");
let alice_chat = alice.create_chat(bob).await;
let mut instance = Message::new(Viewtype::File);
instance
@@ -758,14 +877,14 @@ mod tests {
bob_webxdc.chat_id.accept(bob).await.unwrap();
// Alice advertises herself.
eprintln!("Alice advertises herself");
send_webxdc_realtime_advertisement(alice, alice_webxdc.id)
.await
.unwrap();
bob.recv_msg_trash(&alice.pop_sent_msg().await).await;
// Bob adds alice to gossip peers.
eprintln!("Bob adds alice to gossip peers.");
let members = get_iroh_gossip_peers(bob, bob_webxdc.id)
.await
.unwrap()
@@ -797,7 +916,7 @@ mod tests {
.await
.unwrap();
// Alice sends ephemeral message
eprintln!("Alice sends ephemeral message");
alice
.get_or_try_init_peer_channel()
.await
@@ -845,7 +964,7 @@ mod tests {
.lock()
.get(&bob_topic)
.copied();
// Check that sequence number is persisted when leaving the channel.
eprintln!("Check that sequence number is persisted when leaving the channel");
assert_eq!(bob_sequence_number, bob_sequence_number_after);
bob.get_or_try_init_peer_channel()
@@ -900,17 +1019,19 @@ mod tests {
.await
.unwrap()
.unwrap();
assert!(alice
.iroh
.read()
.await
.as_ref()
.unwrap()
.iroh_channels
.read()
.await
.get(&topic)
.is_none());
assert!(
alice
.iroh
.read()
.await
.as_ref()
.unwrap()
.iroh_channels
.read()
.await
.get(&topic)
.is_none()
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -1,19 +1,22 @@
//! OpenPGP helper module using [rPGP facilities](https://github.com/rpgp/rpgp).
use std::collections::{BTreeMap, HashSet};
use std::io::Cursor;
use std::io::{BufRead, Cursor};
use anyhow::{bail, Context as _, Result};
use anyhow::{Context as _, Result};
use chrono::SubsecRound;
use deltachat_contact_tools::EmailAddress;
use pgp::armor::BlockType;
use pgp::composed::{
Deserializable, KeyType as PgpKeyType, Message, SecretKeyParamsBuilder, SignedPublicKey,
SignedPublicSubKey, SignedSecretKey, StandaloneSignature, SubkeyParamsBuilder,
ArmorOptions, Deserializable, KeyType as PgpKeyType, Message, MessageBuilder,
SecretKeyParamsBuilder, SignedPublicKey, SignedPublicSubKey, SignedSecretKey,
StandaloneSignature, SubkeyParamsBuilder, TheRing,
};
use pgp::crypto::ecc_curve::ECCCurve;
use pgp::crypto::hash::HashAlgorithm;
use pgp::crypto::sym::SymmetricKeyAlgorithm;
use pgp::types::{CompressionAlgorithm, PublicKeyTrait, StringToKey};
use pgp::packet::{SignatureConfig, SignatureType, Subpacket, SubpacketData};
use pgp::types::{CompressionAlgorithm, KeyDetails, Password, PublicKeyTrait, StringToKey};
use rand::thread_rng;
use tokio::runtime::Handle;
@@ -28,7 +31,7 @@ pub const HEADER_SETUPCODE: &str = "passphrase-begin";
const SYMMETRIC_KEY_ALGORITHM: SymmetricKeyAlgorithm = SymmetricKeyAlgorithm::AES128;
/// Preferred cryptographic hash.
const HASH_ALGORITHM: HashAlgorithm = HashAlgorithm::SHA2_256;
const HASH_ALGORITHM: HashAlgorithm = HashAlgorithm::Sha256;
/// Split data from PGP Armored Data as defined in <https://tools.ietf.org/html/rfc4880#section-6.2>.
///
@@ -91,7 +94,7 @@ impl KeyPair {
/// Both secret and public key consist of signing primary key and encryption subkey
/// as [described in the Autocrypt standard](https://autocrypt.org/level1.html#openpgp-based-key-data).
pub(crate) fn create_keypair(addr: EmailAddress) -> Result<KeyPair> {
let signing_key_type = PgpKeyType::EdDSALegacy;
let signing_key_type = PgpKeyType::Ed25519Legacy;
let encryption_key_type = PgpKeyType::ECDH(ECCCurve::Curve25519);
let user_id = format!("<{addr}>");
@@ -107,11 +110,10 @@ pub(crate) fn create_keypair(addr: EmailAddress) -> Result<KeyPair> {
SymmetricKeyAlgorithm::AES128,
])
.preferred_hash_algorithms(smallvec![
HashAlgorithm::SHA2_256,
HashAlgorithm::SHA2_384,
HashAlgorithm::SHA2_512,
HashAlgorithm::SHA2_224,
HashAlgorithm::SHA1,
HashAlgorithm::Sha256,
HashAlgorithm::Sha384,
HashAlgorithm::Sha512,
HashAlgorithm::Sha224,
])
.preferred_compression_algorithms(smallvec![
CompressionAlgorithm::ZLIB,
@@ -132,7 +134,7 @@ pub(crate) fn create_keypair(addr: EmailAddress) -> Result<KeyPair> {
let secret_key = key_params
.generate(&mut rng)
.context("failed to generate the key")?
.sign(&mut rng, || "".into())
.sign(&mut rng, &Password::empty())
.context("failed to sign secret key")?;
secret_key
.verify()
@@ -160,55 +162,73 @@ fn select_pk_for_encryption(key: &SignedPublicKey) -> Option<&SignedPublicSubKey
/// Encrypts `plain` text using `public_keys_for_encryption`
/// and signs it using `private_key_for_signing`.
pub async fn pk_encrypt(
plain: &[u8],
plain: Vec<u8>,
public_keys_for_encryption: Vec<SignedPublicKey>,
private_key_for_signing: Option<SignedSecretKey>,
compress: bool,
) -> Result<String> {
let lit_msg = Message::new_literal_bytes("", plain);
Handle::current()
.spawn_blocking(move || {
let pkeys: Vec<&SignedPublicSubKey> = public_keys_for_encryption
.iter()
.filter_map(select_pk_for_encryption)
.collect();
let mut rng = thread_rng();
let encrypted_msg = if let Some(ref skey) = private_key_for_signing {
let signed_msg = lit_msg.sign(&mut rng, skey, || "".into(), HASH_ALGORITHM)?;
let compressed_msg = if compress {
signed_msg.compress(CompressionAlgorithm::ZLIB)?
} else {
signed_msg
};
compressed_msg.encrypt_to_keys_seipdv1(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys)?
} else {
lit_msg.encrypt_to_keys_seipdv1(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys)?
};
let pkeys = public_keys_for_encryption
.iter()
.filter_map(select_pk_for_encryption);
let encoded_msg = encrypted_msg.to_armored_string(Default::default())?;
let msg = MessageBuilder::from_bytes("", plain);
let mut msg = msg.seipd_v1(&mut rng, SYMMETRIC_KEY_ALGORITHM);
for pkey in pkeys {
msg.encrypt_to_key(&mut rng, &pkey)?;
}
if let Some(ref skey) = private_key_for_signing {
msg.sign(&**skey, Password::empty(), HASH_ALGORITHM);
if compress {
msg.compression(CompressionAlgorithm::ZLIB);
}
}
let encoded_msg = msg.to_armored_string(&mut rng, Default::default())?;
Ok(encoded_msg)
})
.await?
}
/// Signs `plain` text using `private_key_for_signing`.
/// Produces a detached signature for `plain` text using `private_key_for_signing`.
pub fn pk_calc_signature(
plain: &[u8],
plain: Vec<u8>,
private_key_for_signing: &SignedSecretKey,
) -> Result<String> {
let mut rng = thread_rng();
let msg = Message::new_literal_bytes("", plain).sign(
&mut rng,
private_key_for_signing,
|| "".into(),
HASH_ALGORITHM,
let rng = thread_rng();
let mut config = SignatureConfig::from_key(
rng,
&private_key_for_signing.primary_key,
SignatureType::Binary,
)?;
let signature = msg.into_signature().to_armored_string(Default::default())?;
Ok(signature)
config.hashed_subpackets = vec![
Subpacket::regular(SubpacketData::IssuerFingerprint(
private_key_for_signing.fingerprint(),
))?,
Subpacket::critical(SubpacketData::SignatureCreationTime(
chrono::Utc::now().trunc_subsecs(0),
))?,
];
config.unhashed_subpackets = vec![Subpacket::regular(SubpacketData::Issuer(
private_key_for_signing.key_id(),
))?];
let signature = config.sign(
&private_key_for_signing.primary_key,
&Password::empty(),
plain.as_slice(),
)?;
let sig = StandaloneSignature::new(signature);
Ok(sig.to_armored_string(ArmorOptions::default())?)
}
/// Decrypts the message with keys from the private key keyring.
@@ -218,16 +238,27 @@ pub fn pk_calc_signature(
pub fn pk_decrypt(
ctext: Vec<u8>,
private_keys_for_decryption: &[SignedSecretKey],
) -> Result<pgp::composed::Message> {
) -> Result<pgp::composed::Message<'static>> {
let cursor = Cursor::new(ctext);
let (msg, _headers) = Message::from_armor_single(cursor)?;
let (msg, _headers) = Message::from_armor(cursor)?;
let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption.iter().collect();
let empty_pw = Password::empty();
let (msg, _key_ids) = msg.decrypt(|| "".into(), &skeys[..])?;
let ring = TheRing {
secret_keys: skeys,
key_passwords: vec![&empty_pw],
message_password: vec![],
session_keys: vec![],
allow_legacy: false,
};
let (msg, ring_result) = msg.decrypt_the_ring(ring, true)?;
anyhow::ensure!(
!ring_result.secret_keys.is_empty(),
"decryption failed, no matching secret keys"
);
// get_content() will decompress the message if needed,
// but this avoids decompressing it again to check signatures
// remove one layer of compression
let msg = msg.decompress()?;
Ok(msg)
@@ -243,9 +274,9 @@ pub fn valid_signature_fingerprints(
public_keys_for_validation: &[SignedPublicKey],
) -> Result<HashSet<Fingerprint>> {
let mut ret_signature_fingerprints: HashSet<Fingerprint> = Default::default();
if let signed_msg @ pgp::composed::Message::Signed { .. } = msg {
if msg.is_signed() {
for pkey in public_keys_for_validation {
if signed_msg.verify(&pkey.primary_key).is_ok() {
if msg.verify(&pkey.primary_key).is_ok() {
let fp = pkey.dc_fingerprint();
ret_signature_fingerprints.insert(fp);
}
@@ -274,21 +305,17 @@ pub fn pk_validate(
}
/// Symmetric encryption.
pub async fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result<String> {
let lit_msg = Message::new_literal_bytes("", plain);
let passphrase = passphrase.to_string();
pub async fn symm_encrypt(passphrase: &str, plain: Vec<u8>) -> Result<String> {
let passphrase = Password::from(passphrase.to_string());
tokio::task::spawn_blocking(move || {
let mut rng = thread_rng();
let s2k = StringToKey::new_default(&mut rng);
let msg = lit_msg.encrypt_with_password_seipdv1(
&mut rng,
s2k,
SYMMETRIC_KEY_ALGORITHM,
|| passphrase,
)?;
let builder = MessageBuilder::from_bytes("", plain);
let mut builder = builder.seipd_v1(&mut rng, SYMMETRIC_KEY_ALGORITHM);
builder.encrypt_with_password(s2k, &passphrase)?;
let encoded_msg = msg.to_armored_string(Default::default())?;
let encoded_msg = builder.to_armored_string(&mut rng, Default::default())?;
Ok(encoded_msg)
})
@@ -296,20 +323,18 @@ pub async fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result<String> {
}
/// Symmetric decryption.
pub async fn symm_decrypt<T: std::io::Read + std::io::Seek>(
pub async fn symm_decrypt<T: BufRead + std::fmt::Debug + 'static + Send>(
passphrase: &str,
ctext: T,
) -> Result<Vec<u8>> {
let (enc_msg, _) = Message::from_armor_single(ctext)?;
let passphrase = passphrase.to_string();
tokio::task::spawn_blocking(move || {
let msg = enc_msg.decrypt_with_password(|| passphrase)?;
let (enc_msg, _) = Message::from_armor(ctext)?;
let password = Password::from(passphrase);
match msg.get_content()? {
Some(content) => Ok(content),
None => bail!("Decrypted message is empty"),
}
let msg = enc_msg.decrypt_with_password(&password)?;
let res = msg.decompress()?.as_data_vec()?;
Ok(res)
})
.await?
}
@@ -322,16 +347,21 @@ mod tests {
use super::*;
use crate::test_utils::{alice_keypair, bob_keypair};
fn pk_decrypt_and_validate(
ctext: Vec<u8>,
private_keys_for_decryption: &[SignedSecretKey],
fn pk_decrypt_and_validate<'a>(
ctext: &'a [u8],
private_keys_for_decryption: &'a [SignedSecretKey],
public_keys_for_validation: &[SignedPublicKey],
) -> Result<(pgp::composed::Message, HashSet<Fingerprint>)> {
let msg = pk_decrypt(ctext, private_keys_for_decryption)?;
) -> Result<(
pgp::composed::Message<'static>,
HashSet<Fingerprint>,
Vec<u8>,
)> {
let mut msg = pk_decrypt(ctext.to_vec(), private_keys_for_decryption)?;
let content = msg.as_data_vec()?;
let ret_signature_fingerprints =
valid_signature_fingerprints(&msg, public_keys_for_validation)?;
Ok((msg, ret_signature_fingerprints))
Ok((msg, ret_signature_fingerprints, content))
}
#[test]
@@ -407,7 +437,7 @@ mod tests {
let compress = true;
pk_encrypt(
CLEARTEXT,
CLEARTEXT.to_vec(),
keyring,
Some(KEYS.alice_secret.clone()),
compress,
@@ -425,7 +455,7 @@ mod tests {
let keyring = vec![KEYS.alice_public.clone(), KEYS.bob_public.clone()];
let compress = true;
pk_encrypt(CLEARTEXT, keyring, None, compress)
pk_encrypt(CLEARTEXT.to_vec(), keyring, None, compress)
.await
.unwrap()
})
@@ -453,35 +483,34 @@ mod tests {
// Check decrypting as Alice
let decrypt_keyring = vec![KEYS.alice_secret.clone()];
let sig_check_keyring = vec![KEYS.alice_public.clone()];
let (msg, valid_signatures) = pk_decrypt_and_validate(
ctext_signed().await.as_bytes().to_vec(),
let (_msg, valid_signatures, content) = pk_decrypt_and_validate(
ctext_signed().await.as_bytes(),
&decrypt_keyring,
&sig_check_keyring,
)
.unwrap();
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
assert_eq!(content, CLEARTEXT);
assert_eq!(valid_signatures.len(), 1);
// Check decrypting as Bob
let decrypt_keyring = vec![KEYS.bob_secret.clone()];
let sig_check_keyring = vec![KEYS.alice_public.clone()];
let (msg, valid_signatures) = pk_decrypt_and_validate(
ctext_signed().await.as_bytes().to_vec(),
let (_msg, valid_signatures, content) = pk_decrypt_and_validate(
ctext_signed().await.as_bytes(),
&decrypt_keyring,
&sig_check_keyring,
)
.unwrap();
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
assert_eq!(content, CLEARTEXT);
assert_eq!(valid_signatures.len(), 1);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decrypt_no_sig_check() {
let keyring = vec![KEYS.alice_secret.clone()];
let (msg, valid_signatures) =
pk_decrypt_and_validate(ctext_signed().await.as_bytes().to_vec(), &keyring, &[])
.unwrap();
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
let (_msg, valid_signatures, content) =
pk_decrypt_and_validate(ctext_signed().await.as_bytes(), &keyring, &[]).unwrap();
assert_eq!(content, CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
@@ -490,26 +519,23 @@ mod tests {
// The validation does not have the public key of the signer.
let decrypt_keyring = vec![KEYS.bob_secret.clone()];
let sig_check_keyring = vec![KEYS.bob_public.clone()];
let (msg, valid_signatures) = pk_decrypt_and_validate(
ctext_signed().await.as_bytes().to_vec(),
let (_msg, valid_signatures, content) = pk_decrypt_and_validate(
ctext_signed().await.as_bytes(),
&decrypt_keyring,
&sig_check_keyring,
)
.unwrap();
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
assert_eq!(content, CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decrypt_unsigned() {
let decrypt_keyring = vec![KEYS.bob_secret.clone()];
let (msg, valid_signatures) = pk_decrypt_and_validate(
ctext_unsigned().await.as_bytes().to_vec(),
&decrypt_keyring,
&[],
)
.unwrap();
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
let (_msg, valid_signatures, content) =
pk_decrypt_and_validate(ctext_unsigned().await.as_bytes(), &decrypt_keyring, &[])
.unwrap();
assert_eq!(content, CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
}

View File

@@ -11,9 +11,8 @@ use std::sync::Arc;
use anyhow::{Context as _, Result};
use base64::Engine as _;
use pgp::crypto::aead::AeadAlgorithm;
use pgp::crypto::aead::{AeadAlgorithm, ChunkSize};
use pgp::crypto::sym::SymmetricKeyAlgorithm;
use pgp::ser::Serialize;
use rand::thread_rng;
use tokio::sync::RwLock;
@@ -81,18 +80,17 @@ pub(crate) fn encrypt_device_token(device_token: &str) -> Result<String> {
.first()
.context("No encryption subkey found")?;
let padded_device_token = pad_device_token(device_token);
let literal_message = pgp::composed::Message::new_literal("", &padded_device_token);
let mut rng = thread_rng();
let chunk_size = 8;
let encrypted_message = literal_message.encrypt_to_keys_seipdv2(
let mut msg = pgp::composed::MessageBuilder::from_bytes("", padded_device_token).seipd_v2(
&mut rng,
SymmetricKeyAlgorithm::AES128,
AeadAlgorithm::Ocb,
chunk_size,
&[&encryption_subkey],
)?;
let encoded_message = encrypted_message.to_bytes()?;
ChunkSize::C8KiB,
);
msg.encrypt_to_key(&mut rng, &encryption_subkey)?;
let encoded_message = msg.to_vec(&mut rng)?;
Ok(format!(
"openpgp:{}",
base64::engine::general_purpose::STANDARD.encode(encoded_message)

View File

@@ -761,7 +761,9 @@ async fn add_parts(
ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default();
let allow_creation;
if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage
if mime_parser.decrypting_failed {
allow_creation = false;
} else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage
&& is_dc_message == MessengerMessage::No
&& !context.get_config_bool(Config::IsChatmail).await?
{
@@ -2136,11 +2138,14 @@ async fn create_group(
let create_protected = if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
if let VerifiedEncryption::NotVerified(err) = verified_encryption {
warn!(context, "Verification problem: {err:#}.");
let s = format!("{err}. See 'Info' for more details");
mime_parser.replace_msg_by_error(&s);
warn!(
context,
"Creating unprotected group because of the verification problem: {err:#}."
);
ProtectionStatus::Unprotected
} else {
ProtectionStatus::Protected
}
ProtectionStatus::Protected
} else {
ProtectionStatus::Unprotected
};
@@ -2379,12 +2384,17 @@ async fn apply_group_changes(
if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
if let VerifiedEncryption::NotVerified(err) = verified_encryption {
warn!(context, "Verification problem: {err:#}.");
let s = format!("{err}. See 'Info' for more details");
mime_parser.replace_msg_by_error(&s);
}
if !chat.is_protected() {
if chat.is_protected() {
warn!(context, "Verification problem: {err:#}.");
let s = format!("{err}. See 'Info' for more details");
mime_parser.replace_msg_by_error(&s);
} else {
warn!(
context,
"Not marking chat {chat_id} as protected due to verification problem: {err:#}."
);
}
} else if !chat.is_protected() {
chat_id
.set_protection(
context,

View File

@@ -904,7 +904,7 @@ async fn test_concat_multiple_ndns() -> Result<()> {
let raw = include_str!("../../test-data/message/posteo_ndn.eml");
let raw = raw.replace(
"Message-ID: <04422840-f884-3e37-5778-8192fe22d8e1@posteo.de>",
&format!("Message-ID: <{}>", mid),
&format!("Message-ID: <{mid}>"),
);
receive_imf(&t, raw.as_bytes(), false).await?;

View File

@@ -10,6 +10,7 @@ use futures_lite::FutureExt;
use rand::Rng;
use tokio::sync::{oneshot, RwLock, RwLockWriteGuard};
use tokio::task;
use tokio_util::sync::CancellationToken;
use self::connectivity::ConnectivityStore;
use crate::config::{self, Config};
@@ -386,17 +387,17 @@ async fn inbox_loop(
) {
use futures::future::FutureExt;
info!(ctx, "starting inbox loop");
info!(ctx, "Starting inbox loop.");
let ImapConnectionHandlers {
mut connection,
stop_receiver,
stop_token,
} = inbox_handlers;
let ctx1 = ctx.clone();
let fut = async move {
let ctx = ctx1;
if let Err(()) = started.send(()) {
warn!(ctx, "inbox loop, missing started receiver");
warn!(ctx, "Inbox loop, missing started receiver.");
return;
};
@@ -405,9 +406,10 @@ async fn inbox_loop(
let session = if let Some(session) = old_session.take() {
session
} else {
info!(ctx, "Preparing new IMAP session for inbox.");
match connection.prepare(&ctx).await {
Err(err) => {
warn!(ctx, "Failed to prepare INBOX connection: {:#}.", err);
warn!(ctx, "Failed to prepare inbox connection: {err:#}.");
continue;
}
Ok(session) => session,
@@ -415,18 +417,22 @@ async fn inbox_loop(
};
match inbox_fetch_idle(&ctx, &mut connection, session).await {
Err(err) => warn!(ctx, "Failed fetch_idle: {err:#}"),
Err(err) => warn!(ctx, "Failed inbox fetch_idle: {err:#}."),
Ok(session) => {
info!(
ctx,
"IMAP loop iteration for inbox finished, keeping the session."
);
old_session = Some(session);
}
}
}
};
stop_receiver
.recv()
stop_token
.cancelled()
.map(|_| {
info!(ctx, "shutting down inbox loop");
info!(ctx, "Shutting down inbox loop.");
})
.race(fut)
.await;
@@ -695,10 +701,10 @@ async fn simple_imap_loop(
) {
use futures::future::FutureExt;
info!(ctx, "starting simple loop for {}", folder_meaning);
info!(ctx, "Starting simple loop for {folder_meaning}.");
let ImapConnectionHandlers {
mut connection,
stop_receiver,
stop_token,
} = inbox_handlers;
let ctx1 = ctx.clone();
@@ -706,7 +712,10 @@ async fn simple_imap_loop(
let fut = async move {
let ctx = ctx1;
if let Err(()) = started.send(()) {
warn!(&ctx, "simple imap loop, missing started receiver");
warn!(
ctx,
"Simple imap loop for {folder_meaning}, missing started receiver."
);
return;
}
@@ -715,6 +724,7 @@ async fn simple_imap_loop(
let session = if let Some(session) = old_session.take() {
session
} else {
info!(ctx, "Preparing new IMAP session for {folder_meaning}.");
match connection.prepare(&ctx).await {
Err(err) => {
warn!(
@@ -730,16 +740,20 @@ async fn simple_imap_loop(
match fetch_idle(&ctx, &mut connection, session, folder_meaning).await {
Err(err) => warn!(ctx, "Failed fetch_idle: {err:#}"),
Ok(session) => {
info!(
ctx,
"IMAP loop iteration for {folder_meaning} finished, keeping the session"
);
old_session = Some(session);
}
}
}
};
stop_receiver
.recv()
stop_token
.cancelled()
.map(|_| {
info!(ctx, "shutting down simple loop");
info!(ctx, "Shutting down IMAP loop for {folder_meaning}.");
})
.race(fut)
.await;
@@ -755,7 +769,7 @@ async fn smtp_loop(
info!(ctx, "Starting SMTP loop.");
let SmtpConnectionHandlers {
mut connection,
stop_receiver,
stop_token,
idle_interrupt_receiver,
} = smtp_handlers;
@@ -823,8 +837,8 @@ async fn smtp_loop(
}
};
stop_receiver
.recv()
stop_token
.cancelled()
.map(|_| {
info!(ctx, "Shutting down SMTP loop.");
})
@@ -969,9 +983,9 @@ impl Scheduler {
pub(crate) async fn stop(self, context: &Context) {
// Send stop signals to tasks so they can shutdown cleanly.
for b in self.boxes() {
b.conn_state.stop().await.log_err(context).ok();
b.conn_state.stop();
}
self.smtp.stop().await.log_err(context).ok();
self.smtp.stop();
// Actually shutdown tasks.
let timeout_duration = std::time::Duration::from_secs(30);
@@ -1001,8 +1015,8 @@ impl Scheduler {
/// Connection state logic shared between imap and smtp connections.
#[derive(Debug)]
struct ConnectionState {
/// Channel to interrupt the whole connection.
stop_sender: Sender<()>,
/// Cancellation token to interrupt the whole connection.
stop_token: CancellationToken,
/// Channel to interrupt idle.
idle_interrupt_sender: Sender<()>,
/// Mutex to pass connectivity info between IMAP/SMTP threads and the API
@@ -1011,13 +1025,9 @@ struct ConnectionState {
impl ConnectionState {
/// Shutdown this connection completely.
async fn stop(&self) -> Result<()> {
fn stop(&self) {
// Trigger shutdown of the run loop.
self.stop_sender
.send(())
.await
.context("failed to stop, missing receiver")?;
Ok(())
self.stop_token.cancel();
}
fn interrupt(&self) {
@@ -1033,17 +1043,17 @@ pub(crate) struct SmtpConnectionState {
impl SmtpConnectionState {
fn new() -> (Self, SmtpConnectionHandlers) {
let (stop_sender, stop_receiver) = channel::bounded(1);
let stop_token = CancellationToken::new();
let (idle_interrupt_sender, idle_interrupt_receiver) = channel::bounded(1);
let handlers = SmtpConnectionHandlers {
connection: Smtp::new(),
stop_receiver,
stop_token: stop_token.clone(),
idle_interrupt_receiver,
};
let state = ConnectionState {
stop_sender,
stop_token,
idle_interrupt_sender,
connectivity: handlers.connection.connectivity.clone(),
};
@@ -1059,15 +1069,14 @@ impl SmtpConnectionState {
}
/// Shutdown this connection completely.
async fn stop(&self) -> Result<()> {
self.state.stop().await?;
Ok(())
fn stop(&self) {
self.state.stop();
}
}
struct SmtpConnectionHandlers {
connection: Smtp,
stop_receiver: Receiver<()>,
stop_token: CancellationToken,
idle_interrupt_receiver: Receiver<()>,
}
@@ -1079,16 +1088,16 @@ pub(crate) struct ImapConnectionState {
impl ImapConnectionState {
/// Construct a new connection.
async fn new(context: &Context) -> Result<(Self, ImapConnectionHandlers)> {
let (stop_sender, stop_receiver) = channel::bounded(1);
let stop_token = CancellationToken::new();
let (idle_interrupt_sender, idle_interrupt_receiver) = channel::bounded(1);
let handlers = ImapConnectionHandlers {
connection: Imap::new_configured(context, idle_interrupt_receiver).await?,
stop_receiver,
stop_token: stop_token.clone(),
};
let state = ConnectionState {
stop_sender,
stop_token,
idle_interrupt_sender,
connectivity: handlers.connection.connectivity.clone(),
};
@@ -1104,14 +1113,13 @@ impl ImapConnectionState {
}
/// Shutdown this connection completely.
async fn stop(&self) -> Result<()> {
self.state.stop().await?;
Ok(())
fn stop(&self) {
self.state.stop();
}
}
#[derive(Debug)]
struct ImapConnectionHandlers {
connection: Imap,
stop_receiver: Receiver<()>,
stop_token: CancellationToken,
}

View File

@@ -241,7 +241,6 @@ pub(crate) async fn maybe_network_lost(context: &Context, stores: Vec<Connectivi
) {
*connectivity_lock = DetailedConnectivity::Error("Connection lost".to_string());
}
drop(connectivity_lock);
}
context.emit_event(EventType::ConnectivityChanged);
}

View File

@@ -298,7 +298,7 @@ impl Context {
async fn save_message(&self, src_rfc724_mid: &str, dest_rfc724_mid: &String) -> Result<()> {
if let Some((src_msg_id, _)) = message::rfc724_mid_exists(self, src_rfc724_mid).await? {
chat::save_copy_in_self_talk(self, &src_msg_id, dest_rfc724_mid).await?;
chat::save_copy_in_self_talk(self, src_msg_id, dest_rfc724_mid).await?;
}
Ok(())
}
@@ -612,7 +612,7 @@ mod tests {
// the same sync message sent to bob must not be executed
let bob = TestContext::new_bob().await;
bob.recv_msg(&sent_msg).await;
bob.recv_msg_trash(&sent_msg).await;
assert!(!token::exists(&bob, token::Namespace::Auth, "testtoken").await?);
Ok(())

View File

@@ -1,6 +1,7 @@
use anyhow::Result;
use pretty_assertions::assert_eq;
use crate::chat::resend_msgs;
use crate::chat::{
self, add_contact_to_chat, remove_contact_from_chat, send_msg, Chat, ProtectionStatus,
};
@@ -8,10 +9,11 @@ use crate::chatlist::Chatlist;
use crate::config::Config;
use crate::constants::{Chattype, DC_GCL_FOR_FORWARDING};
use crate::contact::{Contact, ContactId, Origin};
use crate::message::Message;
use crate::message::{Message, Viewtype};
use crate::mimefactory::MimeFactory;
use crate::mimeparser::SystemMessage;
use crate::receive_imf::receive_imf;
use crate::securejoin::{get_securejoin_qr, join_securejoin};
use crate::stock_str;
use crate::test_utils::{get_chat_msg, mark_as_verified, TestContext, TestContextManager};
use crate::tools::SystemTime;
@@ -916,12 +918,18 @@ async fn test_verified_member_added_reordering() -> Result<()> {
let bob_sent_message = bob.send_text(bob_chat_id, "Hi").await;
// Fiona receives message from Bob before receiving
// "Member added" message.
// "Member added" message, so unverified group is created.
let fiona_received_message = fiona.recv_msg(&bob_sent_message).await;
assert_eq!(
fiona_received_message.get_text(),
"[The message was sent with non-verified encryption. See 'Info' for more details]"
);
let fiona_chat = Chat::load_from_db(fiona, fiona_received_message.chat_id).await?;
assert_eq!(fiona_received_message.get_text(), "Hi");
assert_eq!(fiona_chat.is_protected(), false);
// Fiona receives late "Member added" message
// and the chat becomes protected.
fiona.recv_msg(&alice_sent_member_added).await;
let fiona_chat = Chat::load_from_db(fiona, fiona_received_message.chat_id).await?;
assert_eq!(fiona_chat.is_protected(), true);
Ok(())
}
@@ -987,13 +995,10 @@ async fn test_verified_lost_member_added() -> Result<()> {
assert_eq!(sent_msg.get_showpadlock(), true);
// The message will not be sent to Fiona.
// Test that Fiona will not be able to decrypt it.
let fiona_rcvd = fiona.recv_msg(&sent).await;
assert_eq!(fiona_rcvd.get_showpadlock(), false);
assert_eq!(
fiona_rcvd.get_text(),
"[...] [This message was encrypted for another setup.]"
);
// Test that Fiona will not be able to decrypt it
// and the message is trashed because
// we don't create groups from undecipherable messages.
fiona.recv_msg_trash(&sent).await;
// Advance the time so Alice does not leave at the same second
// as the group was created.
@@ -1019,6 +1024,77 @@ async fn test_verified_lost_member_added() -> Result<()> {
Ok(())
}
/// Tests handling of resent .xdc arriving before "Member added"
/// in a verified group
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verified_chat_editor_reordering() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let charlie = &tcm.charlie().await;
tcm.execute_securejoin(alice, bob).await;
tcm.section("Alice creates a protected group with Bob");
let alice_chat_id = alice
.create_group_with_members(ProtectionStatus::Protected, "Group", &[bob])
.await;
let alice_sent = alice.send_text(alice_chat_id, "Hi!").await;
let bob_chat_id = bob.recv_msg(&alice_sent).await.chat_id;
tcm.section("Bob sends an .xdc to the chat");
let mut webxdc_instance = Message::new(Viewtype::File);
webxdc_instance.set_file_from_bytes(
bob,
"editor.xdc",
include_bytes!("../../test-data/webxdc/minimal.xdc"),
None,
)?;
let bob_instance_msg_id = send_msg(bob, bob_chat_id, &mut webxdc_instance).await?;
let bob_sent_instance_msg = bob.pop_sent_msg().await;
tcm.section("Alice receives .xdc");
alice.recv_msg(&bob_sent_instance_msg).await;
tcm.section("Alice creates a group QR code");
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await.unwrap();
tcm.section("Charlie scans SecureJoin QR code");
join_securejoin(charlie, &qr).await?;
// vg-request
alice.recv_msg_trash(&charlie.pop_sent_msg().await).await;
// vg-auth-required
charlie.recv_msg_trash(&alice.pop_sent_msg().await).await;
// vg-request-with-auth
alice.recv_msg_trash(&charlie.pop_sent_msg().await).await;
// vg-member-added
let sent_member_added_msg = alice.pop_sent_msg().await;
tcm.section("Bob receives member added message");
bob.recv_msg(&sent_member_added_msg).await;
tcm.section("Bob resends webxdc");
resend_msgs(bob, &[bob_instance_msg_id]).await?;
tcm.section("Charlie receives resent webxdc before member added");
let charlie_received_xdc = charlie.recv_msg(&bob.pop_sent_msg().await).await;
// The message should not be replaced with
// "The message was sent with non-verified encryption." text
// just because it was reordered.
assert_eq!(charlie_received_xdc.viewtype, Viewtype::Webxdc);
tcm.section("Charlie receives member added message");
charlie.recv_msg(&sent_member_added_msg).await;
Ok(())
}
// ============== Helper Functions ==============
async fn assert_verified(this: &TestContext, other: &TestContext, protected: ProtectionStatus) {

View File

@@ -965,7 +965,7 @@ impl Message {
let fingerprint = load_self_public_key(context).await?.dc_fingerprint().hex();
let data = format!("{}-{}", fingerprint, self.rfc724_mid);
let hash = Sha256::digest(data.as_bytes());
Ok(format!("{:x}", hash))
Ok(format!("{hash:x}"))
}
/// Get link attached to an info message.

View File

@@ -124,6 +124,23 @@ async fn test_send_invalid_webxdc() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_draft_invalid_webxdc() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let mut instance = create_webxdc_instance(
&t,
"invalid-no-zip-but-7z.xdc",
include_bytes!("../../test-data/webxdc/invalid-no-zip-but-7z.xdc"),
)?;
// draft should not fail
chat_id.set_draft(&t, Some(&mut instance)).await?;
chat_id.get_draft(&t).await.unwrap();
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_special_webxdc_format() -> Result<()> {
let t = TestContext::new_alice().await;