Compare commits

...
Author SHA1 Message Date
Sebastian Klähn 44e486693d fix: better error message when backup version is too old 2025-06-26 15:15:47 +02:00
link2xt 7ac04d0204 fix: remove display name from get_info()
Display name is rarely needed for debugging,
so there is no need to include it in the logs.
Display name is even already listed in `skip_from_get_info`,
but the test only allowed the values to be skipped
without checking that they are always skipped.
2025-06-25 17:01:51 +00:00
link2xt a40337f4e0 chore(release): prepare for 1.160.0 2025-06-22 12:26:53 +00:00
link2xt b45d9aa464 chore: update rusqlite to 0.36.0 2025-06-21 13:46:00 +00:00
link2xt 48b2e2bc1f chore: sort the list in deny.toml 2025-06-21 13:46:00 +00:00
link2xt 545007aca5 api!: make logging macros private 2025-06-21 11:01:25 +00:00
link2xt 07ce319839 api!(jsonrpc): remove webxdc info from MessageObject 2025-06-18 11:48:32 +00:00
link2xt 0d36c85568 chore: disable some Python lints introduced in ruff 0.12 2025-06-18 10:19:48 +00:00
link2xt 139fbfae85 chore: nightly clippy fixes 2025-06-18 10:19:48 +00:00
Hocuri 0568393157 fix: Don't change ConfiguredAddr when adding a transport (#6804)
Before this PR, ConfiguredAddr (which will be used to store the primary
transport) would have been changed when adding a new transport. Doesn't
matter yet because it's not possible yet to have multiple transports.
But I wanted to fix this bug already so that I'm not suprised by it
later.
2025-06-18 11:19:41 +02:00
iequidoo 7ec732977a fix(contact-tools): Escape commas in vCards' FN, KEY, PHOTO, NOTE (#6912)
Citing @link2xt:
> RFC examples sometimes don't escape commas, but there is errata that fixes some of them.

Also this unescapes commas in all fields. This can lead to, say, an email address with commas, but
anyway the caller should check parsed `VcardContact`'s fields for correctness.
2025-06-14 16:54:29 -03:00
link2xt a8a7cec376 refactor: use CancellationToken instead of a 1-message channel 2025-06-11 14:28:24 +00:00
d2weberandiequidoo 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]andiequidoo 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
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
Hocuri 47b9bfc8bf chore(release): prepare for 1.159.5 2025-05-14 16:58:17 +02:00
Hocuri 21d13e8a9c fix: Don't change webxdc self-addr when saving and loading draft (#6854)
Fix https://github.com/chatmail/core/issues/6621; I also tested on
Android that the webxdc self-addr actually stays the same when staging a
draft and then sending.

Follow-up to https://github.com/chatmail/core/pull/6704; #6704 made sure
that the webxdc self-addr doesn't change when creating a message and
then sending it. This PR here makes sure that the rfc724_mid (which is
needed to compute the self-addr) is saved when setting a draft, so that
it's loaded properly after a call to get_draft().

cc @adbenitez @r10s @Septias
2025-05-14 16:14:35 +02:00
link2xt 079260a7cf chore: update async-smtp to 0.10.2 2025-05-13 16:18:25 +00:00
link2xt fdec78c092 chore: remove duplicate miniz_oxide dependency 2025-05-13 15:25:56 +00:00
link2xt 259ffef0bb chore(release): prepare for 1.159.4 2025-05-13 14:56:09 +00:00
l 6661a0803e chore: update iroh from 0.33.0 to 0.35.0 (#6687) 2025-05-12 20:33:21 +00:00
link2xt c1471bdbd9 docs: add missing documentation to deltachat-rpc-client 2025-05-12 17:39:50 +00:00
Hocuri a981573e48 fix: Fix order of operations when handling "vc-request-with-auth" (#6850) 2025-05-12 16:52:10 +02:00
link2xt 8fb3a7514e fix: replace FuturesUnordered from futures with JoinSet from tokio
FuturesUnordered is likely buggy and iroh previously switched
to JoinSet in <https://github.com/n0-computer/iroh/pull/1647>.
We also have reports with logs of background_fetch getting
stuck so apparently task cancellation after timeout does not work
as intended with FuturesUnordered.
2025-05-10 17:26:05 +00:00
135 changed files with 3393 additions and 2022 deletions
+4 -2
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
+1 -1
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
+2 -1
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
+1 -1
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
View File
@@ -53,3 +53,4 @@ result
# direnv
.envrc
.direnv
.aider*
+89
View File
@@ -1,5 +1,91 @@
# Changelog
## [1.160.0] - 2025-06-22
### API-Changes
- [**breaking**] jsonrpc: remove webxdc info from MessageObject.
Users need to call `get_webxdc_info` separately now
and expect that the call may fail e.g. if WebXDC is not a valid ZIP archive.
- [**breaking**] Deprecate `DC_GCL_VERIFIED_ONLY`.
- [**breaking**] Make logging macros private.
### Features / Changes
- Add more IMAP logging.
- Sort apps by recently-updated ([#6875](https://github.com/chatmail/core/pull/6875)).
- Better error for quoting a message from another chat.
- Put "biography" in the vCard ([#6819](https://github.com/chatmail/core/pull/6819)).
### Fixes
- Do not allow chat creation if decryption failed.
- Remove faulty test ([#6880](https://github.com/chatmail/core/pull/6880)).
- Reduce the scope of the last_full_folder_scan lock in scan_folders.
- Ignore verification error if the chat is not protected yet.
- Create group chats unprotected on verification error.
- `fetch_url`: return err on non 2xx reponses.
- Sort multiple saved messages by timestamp ([#6862](https://github.com/chatmail/core/pull/6862)).
- contact-tools: Escape commas in vCards' FN, KEY, PHOTO, NOTE ([#6912](https://github.com/chatmail/core/pull/6912)).
- Don't change ConfiguredAddr when adding a transport ([#6804](https://github.com/chatmail/core/pull/6804)).
### Build system
- Increase MSRV to 1.85.0.
- Update Doxygen config and layout file.
- Update to rPGP 0.16.0 ([#6719](https://github.com/chatmail/core/pull/6719)).
- Enable async-native-tls/vendored feature.
- Update rusqlite to 0.36.0.
### CI
- Update Rust to 1.87.0.
- nix: Test build on macOS without cross-compilation.
- Use installed toolchain to lint Rust.
### Refactor
- Remove explicit lock drop at the end of scope.
- Use CancellationToken instead of a 1-message channel.
### Documentation
- Add more code style guide references.
## [1.159.5] - 2025-05-14
### Fixes
- Don't change webxdc self-addr when saving and loading draft ([#6854](https://github.com/chatmail/core/pull/6854)).
### Miscellaneous Tasks
- Remove duplicate miniz_oxide dependency.
- Update async-smtp to 0.10.2.
## [1.159.4] - 2025-05-13
### Documentation
- Add missing documentation to deltachat-rpc-client.
### Features / Changes
- Better avatar quality ([#6822](https://github.com/chatmail/core/pull/6822)).
- Update iroh from 0.33.0 to 0.35.0 ([#6687](https://github.com/chatmail/core/pull/6687)).
- Other dependency updates.
### Fixes
- Emit progress(0) in case AEAP is tried.
- Replace `FuturesUnordered` from `futures` with `JoinSet` from `tokio`.
- Fix order of operations when handling "vc-request-with-auth" ([#6850](https://github.com/chatmail/core/pull/6850)).
- Generate rfc724_mid when creating Message ([#6704](https://github.com/chatmail/core/pull/6704))
### Tests
- Profile data is attached to group leave messages.
## [1.159.3] - 2025-04-24
### CI
@@ -6267,3 +6353,6 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[1.159.1]: https://github.com/chatmail/core/compare/v1.159.0..v1.159.1
[1.159.2]: https://github.com/chatmail/core/compare/v1.159.1..v1.159.2
[1.159.3]: https://github.com/chatmail/core/compare/v1.159.2..v1.159.3
[1.159.4]: https://github.com/chatmail/core/compare/v1.159.3..v1.159.4
[1.159.5]: https://github.com/chatmail/core/compare/v1.159.4..v1.159.5
[1.160.0]: https://github.com/chatmail/core/compare/v1.159.5..v1.160.0
Generated
+691 -685
View File
File diff suppressed because it is too large Load Diff
+21 -17
View File
@@ -1,9 +1,9 @@
[package]
name = "deltachat"
version = "1.159.3"
version = "1.160.0"
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>
@@ -43,7 +46,7 @@ async-broadcast = "0.7.2"
async-channel = { workspace = true }
async-imap = { version = "0.10.4", default-features = false, features = ["runtime-tokio", "compress"] }
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.10", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
base64 = { workspace = true }
brotli = { version = "8", default-features=false, features = ["std"] }
@@ -56,25 +59,25 @@ fd-lock = "4"
futures-lite = { workspace = true }
futures = { workspace = true }
hex = "0.4.0"
hickory-resolver = "=0.25.0-alpha.5"
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.33", default-features = false, features = ["net"] }
iroh = { version = "0.33", default-features = false }
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 }
@@ -186,11 +189,11 @@ nu-ansi-term = "0.46"
num-traits = "0.2"
rand = "0.8"
regex = "1.10"
rusqlite = "0.32"
rusqlite = "0.36"
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]
+2 -7
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.
+21
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>
+3 -1
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;
+2 -1
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;
+2 -1
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;
+2 -1
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;
+2 -1
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;
+2 -1
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,
+2 -1
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;
+3 -3
View File
@@ -3,15 +3,15 @@ use criterion::{criterion_group, criterion_main, Criterion};
use deltachat::context::Context;
use deltachat::stock_str::StockStrings;
use deltachat::{info, Event, EventType, Events};
use deltachat::{Event, EventType, Events};
use tempfile::tempdir;
async fn send_events_benchmark(context: &Context) {
let emitter = context.get_event_emitter();
for _i in 0..1_000_000 {
info!(context, "interesting event...");
context.emit_event(EventType::Info("interesting event...".to_string()));
}
info!(context, "DONE");
context.emit_event(EventType::Info("DONE".to_string()));
loop {
match emitter.recv().await.unwrap() {
+2 -2
View File
@@ -76,7 +76,7 @@ impl ContactAddress {
/// Allow converting [`ContactAddress`] to an SQLite type.
impl rusqlite::types::ToSql for ContactAddress {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
let val = rusqlite::types::Value::Text(self.0.to_string());
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
@@ -282,7 +282,7 @@ impl EmailAddress {
}
impl rusqlite::types::ToSql for EmailAddress {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
let val = rusqlite::types::Value::Text(self.to_string());
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
+39 -24
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>,
}
@@ -44,10 +46,15 @@ pub fn make_vcard(contacts: &[VcardContact]) -> String {
Some(datetime.format("%Y%m%dT%H%M%SZ").to_string())
}
fn escape(s: &str) -> String {
s.replace(',', "\\,")
}
let mut res = "".to_string();
for c in contacts {
let addr = &c.addr;
let display_name = c.display_name();
// Mustn't contain ',', but it's easier to escape than to error out.
let addr = escape(&c.addr);
let display_name = escape(c.display_name());
res += &format!(
"BEGIN:VCARD\r\n\
VERSION:4.0\r\n\
@@ -55,10 +62,13 @@ pub fn make_vcard(contacts: &[VcardContact]) -> String {
FN:{display_name}\r\n"
);
if let Some(key) = &c.key {
res += &format!("KEY:data:application/pgp-keys;base64,{key}\r\n");
res += &format!("KEY:data:application/pgp-keys;base64\\,{key}\r\n");
}
if let Some(profile_image) = &c.profile_image {
res += &format!("PHOTO:data:image/jpeg;base64,{profile_image}\r\n");
res += &format!("PHOTO:data:image/jpeg;base64\\,{profile_image}\r\n");
}
if let Some(biography) = &c.biography {
res += &format!("NOTE:{}\r\n", escape(biography));
}
if let Some(timestamp) = format_timestamp(c) {
res += &format!("REV:{timestamp}\r\n");
@@ -79,8 +89,8 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
None
}
}
/// Returns (parameters, value) tuple.
fn vcard_property<'a>(line: &'a str, property: &str) -> Option<(&'a str, &'a str)> {
/// Returns (parameters, raw value) tuple.
fn vcard_property_raw<'a>(line: &'a str, property: &str) -> Option<(&'a str, &'a str)> {
let remainder = remove_prefix(line, property)?;
// If `s` is `EMAIL;TYPE=work:alice@example.com` and `property` is `EMAIL`,
// then `remainder` is now `;TYPE=work:alice@example.com`
@@ -110,23 +120,25 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
}
Some((params, value))
}
/// Returns (parameters, unescaped value) tuple.
fn vcard_property<'a>(line: &'a str, property: &str) -> Option<(&'a str, String)> {
let (params, value) = vcard_property_raw(line, property)?;
// Some fields can't contain commas, but unescape them everywhere for safety.
Some((params, value.replace("\\,", ",")))
}
fn base64_key(line: &str) -> Option<&str> {
let (params, value) = vcard_property(line, "key")?;
let (params, value) = vcard_property_raw(line, "key")?;
if params.eq_ignore_ascii_case("PGP;ENCODING=BASE64")
|| params.eq_ignore_ascii_case("TYPE=PGP;ENCODING=b")
{
return Some(value);
}
if let Some(value) = remove_prefix(value, "data:application/pgp-keys;base64,")
.or_else(|| remove_prefix(value, r"data:application/pgp-keys;base64\,"))
{
return Some(value);
}
None
remove_prefix(value, "data:application/pgp-keys;base64\\,")
// Old Delta Chat format.
.or_else(|| remove_prefix(value, "data:application/pgp-keys;base64,"))
}
fn base64_photo(line: &str) -> Option<&str> {
let (params, value) = vcard_property(line, "photo")?;
let (params, value) = vcard_property_raw(line, "photo")?;
if params.eq_ignore_ascii_case("JPEG;ENCODING=BASE64")
|| params.eq_ignore_ascii_case("ENCODING=BASE64;JPEG")
|| params.eq_ignore_ascii_case("TYPE=JPEG;ENCODING=b")
@@ -136,13 +148,9 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
{
return Some(value);
}
if let Some(value) = remove_prefix(value, "data:image/jpeg;base64,")
.or_else(|| remove_prefix(value, r"data:image/jpeg;base64\,"))
{
return Some(value);
}
None
remove_prefix(value, "data:image/jpeg;base64\\,")
// Old Delta Chat format.
.or_else(|| remove_prefix(value, "data:image/jpeg;base64,"))
}
fn parse_datetime(datetime: &str) -> Result<i64> {
// According to https://www.rfc-editor.org/rfc/rfc6350#section-4.3.5, the timestamp
@@ -186,6 +194,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,18 +214,24 @@ 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") {
let (authname, addr) =
sanitize_name_and_addr(display_name.unwrap_or(""), addr.unwrap_or(""));
let (authname, addr) = sanitize_name_and_addr(
&display_name.unwrap_or_default(),
&addr.unwrap_or_default(),
);
contacts.push(VcardContact {
authname,
addr,
key: key.map(|s| s.to_string()),
profile_image: photo.map(|s| s.to_string()),
biography,
timestamp: datetime
.as_deref()
.context("No timestamp in vcard")
.and_then(parse_datetime),
});
@@ -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),
},
];
@@ -106,8 +108,9 @@ fn test_make_and_parse_vcard() {
VERSION:4.0\r\n\
EMAIL:alice@example.org\r\n\
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\
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\
@@ -246,7 +249,8 @@ END:VCARD",
assert_eq!(contacts[0].profile_image, None);
}
/// Proton at some point slightly changed the format of their vcards
/// Proton at some point slightly changed the format of their vcards.
/// This also tests unescaped commas in PHOTO and KEY (old Delta Chat format).
#[test]
fn test_protonmail_vcard2() {
let contacts = parse_vcard(
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.159.3"
version = "1.160.0"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"
+1001 -445
View File
File diff suppressed because it is too large Load Diff
+233 -2
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>
+4 -3
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
+163 -213
View File
@@ -235,7 +235,10 @@ pub unsafe extern "C" fn dc_set_config(
.log_err(ctx)
.is_ok() as libc::c_int
} else {
match config::Config::from_str(&key) {
match config::Config::from_str(&key)
.context("Invalid config key")
.log_err(ctx)
{
Ok(key) => ctx
.set_config(key, value.as_deref())
.await
@@ -244,10 +247,7 @@ pub unsafe extern "C" fn dc_set_config(
})
.log_err(ctx)
.is_ok() as libc::c_int,
Err(_) => {
warn!(ctx, "dc_set_config(): invalid key");
0
}
Err(_) => 0,
}
}
})
@@ -276,7 +276,10 @@ pub unsafe extern "C" fn dc_get_config(
.unwrap_or_default()
.strdup()
} else {
match config::Config::from_str(&key) {
match config::Config::from_str(&key)
.with_context(|| format!("Invalid key {:?}", &key))
.log_err(ctx)
{
Ok(key) => ctx
.get_config(key)
.await
@@ -285,10 +288,7 @@ pub unsafe extern "C" fn dc_get_config(
.unwrap_or_default()
.unwrap_or_default()
.strdup(),
Err(_) => {
warn!(ctx, "dc_get_config(): invalid key '{}'", &key);
"".strdup()
}
Err(_) => "".strdup(),
}
}
})
@@ -308,18 +308,17 @@ pub unsafe extern "C" fn dc_set_stock_translation(
let ctx = &*context;
block_on(async move {
match StockMessage::from_u32(stock_id) {
Some(id) => match ctx.set_stock_translation(id, msg).await {
Ok(()) => 1,
Err(err) => {
warn!(ctx, "set_stock_translation failed: {err:#}");
0
}
},
None => {
warn!(ctx, "invalid stock message id {stock_id}");
0
}
match StockMessage::from_u32(stock_id)
.with_context(|| format!("Invalid stock message ID {stock_id}"))
.log_err(ctx)
{
Ok(id) => ctx
.set_stock_translation(id, msg)
.await
.context("set_stock_translation failed")
.log_err(ctx)
.is_ok() as libc::c_int,
Err(_) => 0,
}
})
}
@@ -336,15 +335,10 @@ pub unsafe extern "C" fn dc_set_config_from_qr(
let qr = to_string_lossy(qr);
let ctx = &*context;
block_on(async move {
match qr::set_config_from_qr(ctx, &qr).await {
Ok(()) => 1,
Err(err) => {
error!(ctx, "Failed to create account from QR code: {err:#}");
0
}
}
})
block_on(qr::set_config_from_qr(ctx, &qr))
.context("Failed to create account from QR code")
.log_err(ctx)
.is_ok() as libc::c_int
}
#[no_mangle]
@@ -354,15 +348,13 @@ pub unsafe extern "C" fn dc_get_info(context: *const dc_context_t) -> *mut libc:
return "".strdup();
}
let ctx = &*context;
block_on(async move {
match ctx.get_info().await {
Ok(info) => render_info(info).unwrap_or_default().strdup(),
Err(err) => {
warn!(ctx, "failed to get info: {err:#}");
"".strdup()
}
}
})
match block_on(ctx.get_info())
.context("Failed to get info")
.log_err(ctx)
{
Ok(info) => render_info(info).unwrap_or_default().strdup(),
Err(_) => "".strdup(),
}
}
fn render_info(
@@ -395,15 +387,13 @@ pub unsafe extern "C" fn dc_get_connectivity_html(
return "".strdup();
}
let ctx = &*context;
block_on(async move {
match ctx.get_connectivity_html().await {
Ok(html) => html.strdup(),
Err(err) => {
error!(ctx, "Failed to get connectivity html: {err:#}");
"".strdup()
}
}
})
match block_on(ctx.get_connectivity_html())
.context("Failed to get connectivity html")
.log_err(ctx)
{
Ok(html) => html.strdup(),
Err(_) => "".strdup(),
}
}
#[no_mangle]
@@ -1255,22 +1245,19 @@ pub unsafe extern "C" fn dc_get_draft(context: *mut dc_context_t, chat_id: u32)
}
let ctx = &*context;
block_on(async move {
match ChatId::new(chat_id).get_draft(ctx).await {
Ok(Some(draft)) => {
let ffi_msg = MessageWrapper {
context,
message: draft,
};
Box::into_raw(Box::new(ffi_msg))
}
Ok(None) => ptr::null_mut(),
Err(err) => {
error!(ctx, "Failed to get draft for chat #{chat_id}: {err:#}");
ptr::null_mut()
}
match block_on(ChatId::new(chat_id).get_draft(ctx))
.with_context(|| format!("Failed to get draft for chat #{chat_id}"))
.unwrap_or_default()
{
Some(draft) => {
let ffi_msg = MessageWrapper {
context,
message: draft,
};
Box::into_raw(Box::new(ffi_msg))
}
})
None => ptr::null_mut(),
}
}
#[no_mangle]
@@ -1526,10 +1513,7 @@ pub unsafe extern "C" fn dc_set_chat_visibility(
1 => ChatVisibility::Archived,
2 => ChatVisibility::Pinned,
_ => {
warn!(
ctx,
"ignoring careless call to dc_set_chat_visibility(): unknown archived state",
);
eprintln!("ignoring careless call to dc_set_chat_visibility(): unknown archived state");
return;
}
};
@@ -1683,10 +1667,11 @@ pub unsafe extern "C" fn dc_create_group_chat(
return 0;
}
let ctx = &*context;
let protect = if let Some(s) = ProtectionStatus::from_i32(protect) {
s
} else {
warn!(ctx, "bad protect-value for dc_create_group_chat()");
let Some(protect) = ProtectionStatus::from_i32(protect)
.context("Bad protect-value for dc_create_group_chat()")
.log_err(ctx)
.ok()
else {
return 0;
};
@@ -1832,23 +1817,20 @@ pub unsafe extern "C" fn dc_set_chat_mute_duration(
return 0;
}
let ctx = &*context;
let muteDuration = match duration {
let mute_duration = match duration {
0 => MuteDuration::NotMuted,
-1 => MuteDuration::Forever,
n if n > 0 => SystemTime::now()
.checked_add(Duration::from_secs(duration as u64))
.map_or(MuteDuration::Forever, MuteDuration::Until),
_ => {
warn!(
ctx,
"dc_chat_set_mute_duration(): Can not use negative duration other than -1",
);
eprintln!("dc_chat_set_mute_duration(): Can not use negative duration other than -1");
return 0;
}
};
block_on(async move {
chat::set_muted(ctx, ChatId::new(chat_id), muteDuration)
chat::set_muted(ctx, ChatId::new(chat_id), mute_duration)
.await
.map(|_| 1)
.unwrap_or_log_default(ctx, "Failed to set mute duration")
@@ -1866,16 +1848,10 @@ pub unsafe extern "C" fn dc_get_chat_encrinfo(
}
let ctx = &*context;
block_on(async move {
ChatId::new(chat_id)
.get_encryption_info(ctx)
.await
.map(|s| s.strdup())
.unwrap_or_else(|e| {
error!(ctx, "{e:#}");
ptr::null_mut()
})
})
block_on(ChatId::new(chat_id).get_encryption_info(ctx))
.map(|s| s.strdup())
.log_err(ctx)
.unwrap_or(ptr::null_mut())
}
#[no_mangle]
@@ -2032,12 +2008,10 @@ pub unsafe extern "C" fn dc_resend_msgs(
let ctx = &*context;
let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt);
if let Err(err) = block_on(chat::resend_msgs(ctx, &msg_ids)) {
error!(ctx, "Resending failed: {err:#}");
0
} else {
1
}
block_on(chat::resend_msgs(ctx, &msg_ids))
.context("Resending failed")
.log_err(ctx)
.is_ok() as libc::c_int
}
#[no_mangle]
@@ -2067,26 +2041,22 @@ pub unsafe extern "C" fn dc_get_msg(context: *mut dc_context_t, msg_id: u32) ->
}
let ctx = &*context;
block_on(async move {
let message = match message::Message::load_from_db(ctx, MsgId::new(msg_id)).await {
Ok(msg) => msg,
Err(e) => {
if msg_id <= constants::DC_MSG_ID_LAST_SPECIAL {
// C-core API returns empty messages, do the same
warn!(
ctx,
"dc_get_msg called with special msg_id={msg_id}, returning empty msg"
);
message::Message::new(Viewtype::default())
} else {
warn!(ctx, "dc_get_msg could not retrieve msg_id {msg_id}: {e:#}");
return ptr::null_mut();
}
let message = match block_on(message::Message::load_from_db(ctx, MsgId::new(msg_id)))
.with_context(|| format!("dc_get_msg could not rectieve msg_id {msg_id}"))
.log_err(ctx)
{
Ok(msg) => msg,
Err(_) => {
if msg_id <= constants::DC_MSG_ID_LAST_SPECIAL {
// C-core API returns empty messages, do the same
message::Message::new(Viewtype::default())
} else {
return ptr::null_mut();
}
};
let ffi_msg = MessageWrapper { context, message };
Box::into_raw(Box::new(ffi_msg))
})
}
};
let ffi_msg = MessageWrapper { context, message };
Box::into_raw(Box::new(ffi_msg))
}
#[no_mangle]
@@ -2316,15 +2286,10 @@ pub unsafe extern "C" fn dc_get_contact_encrinfo(
}
let ctx = &*context;
block_on(async move {
Contact::get_encrinfo(ctx, ContactId::new(contact_id))
.await
.map(|s| s.strdup())
.unwrap_or_else(|e| {
error!(ctx, "{e:#}");
ptr::null_mut()
})
})
block_on(Contact::get_encrinfo(ctx, ContactId::new(contact_id)))
.map(|s| s.strdup())
.log_err(ctx)
.unwrap_or(ptr::null_mut())
}
#[no_mangle]
@@ -2339,15 +2304,10 @@ pub unsafe extern "C" fn dc_delete_contact(
}
let ctx = &*context;
block_on(async move {
match Contact::delete(ctx, contact_id).await {
Ok(_) => 1,
Err(err) => {
error!(ctx, "cannot delete contact: {err:#}");
0
}
}
})
block_on(Contact::delete(ctx, contact_id))
.context("Cannot delete contact")
.log_err(ctx)
.is_ok() as libc::c_int
}
#[no_mangle]
@@ -2418,17 +2378,13 @@ pub unsafe extern "C" fn dc_imex_has_backup(
}
let ctx = &*context;
block_on(async move {
match imex::has_backup(ctx, to_string_lossy(dir).as_ref()).await {
Ok(res) => res.strdup(),
Err(err) => {
// do not bubble up error to the user,
// the ui will expect that the file does not exist or cannot be accessed
warn!(ctx, "dc_imex_has_backup: {err:#}");
ptr::null_mut()
}
}
})
match block_on(imex::has_backup(ctx, to_string_lossy(dir).as_ref()))
.context("dc_imex_has_backup")
.log_err(ctx)
{
Ok(res) => res.strdup(),
Err(_) => ptr::null_mut(),
}
}
#[no_mangle]
@@ -2439,15 +2395,13 @@ pub unsafe extern "C" fn dc_initiate_key_transfer(context: *mut dc_context_t) ->
}
let ctx = &*context;
block_on(async move {
match imex::initiate_key_transfer(ctx).await {
Ok(res) => res.strdup(),
Err(err) => {
error!(ctx, "dc_initiate_key_transfer(): {err:#}");
ptr::null_mut()
}
}
})
match block_on(imex::initiate_key_transfer(ctx))
.context("dc_initiate_key_transfer()")
.log_err(ctx)
{
Ok(res) => res.strdup(),
Err(_) => ptr::null_mut(),
}
}
#[no_mangle]
@@ -2462,17 +2416,14 @@ pub unsafe extern "C" fn dc_continue_key_transfer(
}
let ctx = &*context;
block_on(async move {
match imex::continue_key_transfer(ctx, MsgId::new(msg_id), &to_string_lossy(setup_code))
.await
{
Ok(()) => 1,
Err(err) => {
warn!(ctx, "dc_continue_key_transfer: {err:#}");
0
}
}
})
block_on(imex::continue_key_transfer(
ctx,
MsgId::new(msg_id),
&to_string_lossy(setup_code),
))
.context("dc_continue_key_transfer")
.log_err(ctx)
.is_ok() as libc::c_int
}
#[no_mangle]
@@ -2916,12 +2867,14 @@ pub unsafe extern "C" fn dc_chatlist_get_chat_id(
}
let ffi_list = &*chatlist;
let ctx = &*ffi_list.context;
match ffi_list.list.get_chat_id(index) {
match ffi_list
.list
.get_chat_id(index)
.context("get_chat_id failed")
.log_err(ctx)
{
Ok(chat_id) => chat_id.to_u32(),
Err(err) => {
warn!(ctx, "get_chat_id failed: {err:#}");
0
}
Err(_) => 0,
}
}
@@ -2936,12 +2889,14 @@ pub unsafe extern "C" fn dc_chatlist_get_msg_id(
}
let ffi_list = &*chatlist;
let ctx = &*ffi_list.context;
match ffi_list.list.get_msg_id(index) {
match ffi_list
.list
.get_msg_id(index)
.context("get_msg_id failed")
.log_err(ctx)
{
Ok(msg_id) => msg_id.map_or(0, |msg_id| msg_id.to_u32()),
Err(err) => {
warn!(ctx, "get_msg_id failed: {err:#}");
0
}
Err(_) => 0,
}
}
@@ -3095,13 +3050,16 @@ pub unsafe extern "C" fn dc_chat_get_profile_image(chat: *mut dc_chat_t) -> *mut
let ffi_chat = &*chat;
block_on(async move {
match ffi_chat.chat.get_profile_image(&ffi_chat.context).await {
Ok(Some(p)) => p.to_string_lossy().strdup(),
Ok(None) => ptr::null_mut(),
Err(err) => {
error!(ffi_chat.context, "failed to get profile image: {err:#}");
ptr::null_mut()
}
match ffi_chat
.chat
.get_profile_image(&ffi_chat.context)
.await
.context("Failed to get profile image")
.log_err(&ffi_chat.context)
.unwrap_or_default()
{
Some(p) => p.to_string_lossy().strdup(),
None => ptr::null_mut(),
}
})
}
@@ -3258,22 +3216,20 @@ pub unsafe extern "C" fn dc_chat_get_info_json(
let ctx = &*context;
block_on(async move {
let chat = match chat::Chat::load_from_db(ctx, ChatId::new(chat_id)).await {
Ok(chat) => chat,
Err(err) => {
error!(ctx, "dc_get_chat_info_json() failed to load chat: {err:#}");
return "".strdup();
}
let Ok(chat) = chat::Chat::load_from_db(ctx, ChatId::new(chat_id))
.await
.context("dc_get_chat_info_json() failed to load chat")
.log_err(ctx)
else {
return "".strdup();
};
let info = match chat.get_info(ctx).await {
Ok(info) => info,
Err(err) => {
error!(
ctx,
"dc_get_chat_info_json() failed to get chat info: {err:#}"
);
return "".strdup();
}
let Ok(info) = chat
.get_info(ctx)
.await
.context("dc_get_chat_info_json() failed to get chat info")
.log_err(ctx)
else {
return "".strdup();
};
serde_json::to_string(&info)
.unwrap_or_log_default(ctx, "dc_get_chat_info_json() failed to serialise to json")
@@ -3533,18 +3489,15 @@ pub unsafe extern "C" fn dc_msg_get_webxdc_info(msg: *mut dc_msg_t) -> *mut libc
let ffi_msg = &*msg;
let ctx = &*ffi_msg.context;
block_on(async move {
let info = match ffi_msg.message.get_webxdc_info(ctx).await {
Ok(info) => info,
Err(err) => {
error!(ctx, "dc_msg_get_webxdc_info() failed to get info: {err:#}");
return "".strdup();
}
};
serde_json::to_string(&info)
.unwrap_or_log_default(ctx, "dc_msg_get_webxdc_info() failed to serialise to json")
.strdup()
})
let Ok(info) = block_on(ffi_msg.message.get_webxdc_info(ctx))
.context("dc_msg_get_webxdc_info() failed to get info")
.log_err(ctx)
else {
return "".strdup();
};
serde_json::to_string(&info)
.unwrap_or_log_default(ctx, "dc_msg_get_webxdc_info() failed to serialise to json")
.strdup()
}
#[no_mangle]
@@ -4574,13 +4527,10 @@ trait ResultExt<T, E> {
impl<T: Default, E: std::fmt::Display> ResultExt<T, E> for Result<T, E> {
fn unwrap_or_log_default(self, context: &context::Context, message: &str) -> T {
match self {
Ok(t) => t,
Err(err) => {
error!(context, "{message}: {err:#}");
Default::default()
}
}
self.map_err(|err| anyhow::anyhow!("{err:#}"))
.with_context(|| message.to_string())
.log_err(context)
.unwrap_or_default()
}
}
+2 -2
View File
@@ -34,7 +34,7 @@ pub enum Meaning {
}
impl Lot {
pub fn get_text1(&self) -> Option<Cow<str>> {
pub fn get_text1(&self) -> Option<Cow<'_, str>> {
match self {
Self::Summary(summary) => match &summary.prefix {
None => None,
@@ -66,7 +66,7 @@ impl Lot {
}
}
pub fn get_text2(&self) -> Option<Cow<str>> {
pub fn get_text2(&self) -> Option<Cow<'_, str>> {
match self {
Self::Summary(summary) => Some(summary.truncated_text(160)),
Self::Qr(_) => None,
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.159.3"
version = "1.160.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
license = "MPL-2.0"
+5 -7
View File
@@ -19,6 +19,7 @@ use deltachat::constants::DC_MSG_ID_DAYMARKER;
use deltachat::contact::{may_be_valid_addr, Contact, ContactId, Origin};
use deltachat::context::get_info;
use deltachat::ephemeral::Timer;
use deltachat::imex;
use deltachat::location;
use deltachat::message::get_msg_read_receipts;
use deltachat::message::{
@@ -35,7 +36,6 @@ use deltachat::securejoin;
use deltachat::stock_str::StockMessage;
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::EventEmitter;
use deltachat::{imex, info};
use sanitize_filename::is_sanitized;
use tokio::fs;
use tokio::sync::{watch, Mutex, RwLock};
@@ -1919,12 +1919,10 @@ impl CommandApi {
instance_msg_id: u32,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let fut = send_webxdc_realtime_advertisement(&ctx, MsgId::new(instance_msg_id)).await?;
if let Some(fut) = fut {
tokio::spawn(async move {
fut.await.ok();
info!(ctx, "send_webxdc_realtime_advertisement done")
});
if let Some(fut) =
send_webxdc_realtime_advertisement(&ctx, MsgId::new(instance_msg_id)).await?
{
tokio::spawn(fut);
}
Ok(())
}
+1 -10
View File
@@ -19,10 +19,10 @@ use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
use super::contact::ContactObject;
use super::reactions::JSONRPCReactions;
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 },
@@ -90,8 +90,6 @@ pub struct MessageObject {
file_bytes: u64,
file_name: Option<String>,
webxdc_info: Option<WebxdcMessageInfo>,
webxdc_href: Option<String>,
download_state: DownloadState,
@@ -142,12 +140,6 @@ impl MessageObject {
let file_bytes = message.get_filebytes(context).await?.unwrap_or_default();
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?)
} else {
None
};
let parent_id = message.parent(context).await?.map(|m| m.get_id().to_u32());
let download_state = message.download_state().into();
@@ -261,7 +253,6 @@ impl MessageObject {
file_mime: message.get_filemime(),
file_bytes,
file_name: message.get_filename(),
webxdc_info,
// On a WebxdcInfoMessage this might include a hash holding
// information about a specific position or state in a webxdc app
+12 -12
View File
@@ -2,24 +2,24 @@
"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": {
@@ -54,5 +54,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.159.3"
"version": "1.160.0"
}
@@ -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"),
);
@@ -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`,
);
+12 -6
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 = "";
+8 -9
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);
});
+10 -10
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) {
@@ -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);
}
}
}
},
);
});
}
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "1.159.3"
version = "1.160.0"
license = "MPL-2.0"
edition = "2021"
repository = "https://github.com/chatmail/core"
@@ -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"] }
+5 -14
View File
@@ -120,7 +120,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
} else {
let rs = context.sql().get_raw_config("import_spec").await.unwrap();
if rs.is_none() {
error!(context, "Import: No file or folder given.");
eprintln!("Import: No file or folder given.");
return false;
}
real_spec = rs.unwrap();
@@ -149,7 +149,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
}
}
} else {
error!(context, "Import: Cannot open directory \"{}\".", &real_spec);
eprintln!("Import: Cannot open directory \"{}\".", &real_spec);
return false;
}
}
@@ -493,7 +493,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"send-backup" => {
let provider = BackupProvider::prepare(&context).await?;
let qr = format_backup(&provider.qr())?;
println!("QR code: {}", qr);
println!("QR code: {qr}");
qr2term::print_qr(qr.as_str())?;
provider.await?;
}
@@ -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());
}
+8 -9
View File
@@ -5,7 +5,6 @@
//! Usage: cargo run --example repl --release -- <databasefile>
//! All further options can be set using the set-command (type ? for help).
#[macro_use]
extern crate deltachat;
use std::borrow::Cow::{self, Borrowed, Owned};
@@ -41,25 +40,25 @@ fn receive_event(event: EventType) {
match event {
EventType::Info(msg) => {
/* do not show the event as this would fill the screen */
info!("{}", msg);
info!("{msg}");
}
EventType::SmtpConnected(msg) => {
info!("[SMTP_CONNECTED] {}", msg);
info!("[SMTP_CONNECTED] {msg}");
}
EventType::ImapConnected(msg) => {
info!("[IMAP_CONNECTED] {}", msg);
info!("[IMAP_CONNECTED] {msg}");
}
EventType::SmtpMessageSent(msg) => {
info!("[SMTP_MESSAGE_SENT] {}", msg);
info!("[SMTP_MESSAGE_SENT] {msg}");
}
EventType::Warning(msg) => {
warn!("{}", msg);
warn!("{msg}");
}
EventType::Error(msg) => {
error!("{}", msg);
error!("{msg}");
}
EventType::ErrorSelfNotInGroup(msg) => {
error!("[SELF_NOT_IN_GROUP] {}", msg);
error!("[SELF_NOT_IN_GROUP] {msg}");
}
EventType::MsgsChanged { chat_id, msg_id } => {
info!(
@@ -124,7 +123,7 @@ fn receive_event(event: EventType) {
);
}
_ => {
info!("Received {:?}", event);
info!("Received {event:?}");
}
}
}
+4 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "1.159.3"
version = "1.160.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -66,6 +66,9 @@ lint.select = [
"RUF006" # asyncio-dangling-task
]
lint.ignore = [
"PLC0415" # `import` should be at the top-level of a file
]
line-length = 120
[tool.isort]
@@ -1,4 +1,4 @@
"""Delta Chat JSON-RPC high-level API"""
"""Delta Chat JSON-RPC high-level API."""
from ._utils import AttrDict, run_bot_cli, run_client_cli
from .account import Account
@@ -115,7 +115,7 @@ def _run_cli(
def extract_addr(text: str) -> str:
"""extract email address from the given text."""
"""Extract email address from the given text."""
match = re.match(r".*\((.+@.+)\)", text)
if match:
text = match.group(1)
@@ -124,7 +124,7 @@ def extract_addr(text: str) -> str:
def parse_system_image_changed(text: str) -> Optional[Tuple[str, bool]]:
"""return image changed/deleted info from parsing the given system message text."""
"""Return image changed/deleted info from parsing the given system message text."""
text = text.lower()
match = re.match(r"group image (changed|deleted) by (.+).", text)
if match:
@@ -143,7 +143,7 @@ def parse_system_title_changed(text: str) -> Optional[Tuple[str, str]]:
def parse_system_add_remove(text: str) -> Optional[Tuple[str, str, str]]:
"""return add/remove info from parsing the given system message text.
"""Return add/remove info from parsing the given system message text.
returns a (action, affected, actor) tuple.
"""
@@ -1,3 +1,5 @@
"""Account module."""
from __future__ import annotations
from dataclasses import dataclass
@@ -34,7 +36,10 @@ class Account:
return next_event
def clear_all_events(self):
"""Removes all queued-up events for a given account. Useful for tests."""
"""Remove all queued-up events for a given account.
Useful for tests.
"""
self._rpc.clear_all_events(self.id)
def remove(self) -> None:
@@ -43,7 +48,9 @@ class Account:
def clone(self) -> "Account":
"""Clone given account.
This uses backup-transfer via iroh, i.e. the 'Add second device' feature."""
This uses backup-transfer via iroh, i.e. the 'Add second device' feature.
"""
future = self._rpc.provide_backup.future(self.id)
qr = self._rpc.get_backup_qr(self.id)
new_account = self.manager.add_account()
@@ -80,7 +87,7 @@ class Account:
return self._rpc.get_config(self.id, key)
def update_config(self, **kwargs) -> None:
"""update config values."""
"""Update config values."""
for key, value in kwargs.items():
self.set_config(key, value)
@@ -99,10 +106,12 @@ class Account:
"""Parse QR code contents.
This function takes the raw text scanned
and checks what can be done with it."""
and checks what can be done with it.
"""
return self._rpc.check_qr(self.id, qr)
def set_config_from_qr(self, qr: str):
"""Set configuration values from a QR code."""
self._rpc.set_config_from_qr(self.id, qr)
@futuremethod
@@ -117,7 +126,7 @@ class Account:
@futuremethod
def list_transports(self):
"""Returns the list of all email accounts that are used as a transport in the current profile."""
"""Return the list of all email accounts that are used as a transport in the current profile."""
transports = yield self._rpc.list_transports.future(self.id)
return transports
@@ -158,7 +167,8 @@ class Account:
def import_vcard(self, vcard: str) -> list[Contact]:
"""Import vCard.
Return created or modified contacts in the order they appear in vCard."""
Return created or modified contacts in the order they appear in vCard.
"""
contact_ids = self._rpc.import_vcard_contents(self.id, vcard)
return [Contact(self, contact_id) for contact_id in contact_ids]
@@ -201,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.
@@ -210,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
@@ -227,12 +234,12 @@ class Account:
@property
def self_contact(self) -> Contact:
"""This account's identity as a Contact."""
"""Account's identity as a Contact."""
return Contact(self, SpecialContactId.SELF)
@property
def device_contact(self) -> Chat:
"""This account's device contact."""
"""Account's device contact."""
return Contact(self, SpecialContactId.DEVICE)
def get_chatlist(
@@ -290,8 +297,7 @@ class Account:
return Chat(self, chat_id)
def secure_join(self, qrdata: str) -> Chat:
"""Continue a Setup-Contact or Verified-Group-Invite protocol started on
another device.
"""Continue a Setup-Contact or Verified-Group-Invite protocol started on another device.
The function returns immediately and the handshake runs in background, sending
and receiving several messages.
@@ -361,22 +367,26 @@ class Account:
def wait_for_incoming_msg(self):
"""Wait for incoming message and return it.
Consumes all events before the next incoming message event."""
Consumes all events before the next incoming message event.
"""
return self.get_message_by_id(self.wait_for_incoming_msg_event().msg_id)
def wait_for_securejoin_inviter_success(self):
"""Wait until SecureJoin process finishes successfully on the inviter side."""
while True:
event = self.wait_for_event()
if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
def wait_for_securejoin_joiner_success(self):
"""Wait until SecureJoin process finishes successfully on the joiner side."""
while True:
event = self.wait_for_event()
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
break
def wait_for_reactions_changed(self):
"""Wait for reaction change event."""
return self.wait_for_event(EventType.REACTIONS_CHANGED)
def get_fresh_messages_in_arrival_order(self) -> list[Message]:
@@ -1,3 +1,5 @@
"""Chat module."""
from __future__ import annotations
import calendar
@@ -89,7 +91,8 @@ class Chat:
def set_ephemeral_timer(self, timer: int) -> None:
"""Set ephemeral timer of this chat in seconds.
0 means the timer is disabled, use 1 for immediate deletion."""
0 means the timer is disabled, use 1 for immediate deletion.
"""
self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
def get_encryption_info(self) -> str:
@@ -199,12 +202,12 @@ class Chat:
return snapshot
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> list[Message]:
"""get the list of messages in this chat."""
"""Get the list of messages in this chat."""
msgs = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
return [Message(self.account, msg_id) for msg_id in msgs]
def get_fresh_message_count(self) -> int:
"""Get number of fresh messages in this chat"""
"""Get number of fresh messages in this chat."""
return self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
def mark_noticed(self) -> None:
@@ -48,6 +48,7 @@ class Client:
self.add_hooks(hooks or [])
def add_hooks(self, hooks: Iterable[tuple[Callable, Union[type, EventFilter]]]) -> None:
"""Register multiple hooks."""
for hook, event in hooks:
self.add_hook(hook, event)
@@ -77,9 +78,11 @@ class Client:
self._hooks.get(type(event), set()).remove((hook, event))
def is_configured(self) -> bool:
"""Return True if the client is configured."""
return self.account.is_configured()
def configure(self, email: str, password: str, **kwargs) -> None:
"""Configure the client."""
self.account.set_config("addr", email)
self.account.set_config("mail_pw", password)
for key, value in kwargs.items():
@@ -198,5 +201,6 @@ class Bot(Client):
"""Simple bot implementation that listens to events of a single account."""
def configure(self, email: str, password: str, **kwargs) -> None:
"""Configure the bot."""
kwargs.setdefault("bot", "1")
super().configure(email, password, **kwargs)
@@ -1,14 +1,19 @@
"""Constants module."""
from enum import Enum, IntEnum
COMMAND_PREFIX = "/"
class ContactFlag(IntEnum):
VERIFIED_ONLY = 0x01
"""Bit flags for get_contacts() method."""
ADD_SELF = 0x02
class ChatlistFlag(IntEnum):
"""Bit flags for get_chatlist() method."""
ARCHIVED_ONLY = 0x01
NO_SPECIALS = 0x02
ADD_ALLDONE_HINT = 0x04
@@ -16,6 +21,8 @@ class ChatlistFlag(IntEnum):
class SpecialContactId(IntEnum):
"""Special contact IDs."""
SELF = 1
INFO = 2 # centered messages as "member added", used in all chats
DEVICE = 5 # messages "update info" in the device-chat
@@ -23,7 +30,7 @@ class SpecialContactId(IntEnum):
class EventType(str, Enum):
"""Core event types"""
"""Core event types."""
INFO = "Info"
SMTP_CONNECTED = "SmtpConnected"
@@ -71,7 +78,7 @@ class EventType(str, Enum):
class ChatId(IntEnum):
"""Special chat ids"""
"""Special chat IDs."""
TRASH = 3
ARCHIVED_LINK = 6
@@ -80,7 +87,7 @@ class ChatId(IntEnum):
class ChatType(IntEnum):
"""Chat types"""
"""Chat type."""
UNDEFINED = 0
SINGLE = 100
@@ -90,7 +97,7 @@ class ChatType(IntEnum):
class ChatVisibility(str, Enum):
"""Chat visibility types"""
"""Chat visibility types."""
NORMAL = "Normal"
ARCHIVED = "Archived"
@@ -98,7 +105,7 @@ class ChatVisibility(str, Enum):
class DownloadState(str, Enum):
"""Message download state"""
"""Message download state."""
DONE = "Done"
AVAILABLE = "Available"
@@ -159,14 +166,14 @@ class MessageState(IntEnum):
class MessageId(IntEnum):
"""Special message ids"""
"""Special message IDs."""
DAYMARKER = 9
LAST_SPECIAL = 9
class CertificateChecks(IntEnum):
"""Certificate checks mode"""
"""Certificate checks mode."""
AUTOMATIC = 0
STRICT = 1
@@ -174,7 +181,7 @@ class CertificateChecks(IntEnum):
class Connectivity(IntEnum):
"""Connectivity states"""
"""Connectivity states."""
NOT_CONNECTED = 1000
CONNECTING = 2000
@@ -183,7 +190,7 @@ class Connectivity(IntEnum):
class KeyGenType(IntEnum):
"""Type of the key to generate"""
"""Type of the key to generate."""
DEFAULT = 0
RSA2048 = 1
@@ -193,21 +200,21 @@ class KeyGenType(IntEnum):
# "Lp" means "login parameters"
class LpAuthFlag(IntEnum):
"""Authorization flags"""
"""Authorization flags."""
OAUTH2 = 0x2
NORMAL = 0x4
class MediaQuality(IntEnum):
"""Media quality setting"""
"""Media quality setting."""
BALANCED = 0
WORSE = 1
class ProviderStatus(IntEnum):
"""Provider status according to manual testing"""
"""Provider status according to manual testing."""
OK = 1
PREPARATION = 2
@@ -215,7 +222,7 @@ class ProviderStatus(IntEnum):
class PushNotifyState(IntEnum):
"""Push notifications state"""
"""Push notifications state."""
NOT_CONNECTED = 0
HEARTBEAT = 1
@@ -223,7 +230,7 @@ class PushNotifyState(IntEnum):
class ShowEmails(IntEnum):
"""Show emails mode"""
"""Show emails mode."""
OFF = 0
ACCEPTED_CONTACTS = 1
@@ -231,7 +238,7 @@ class ShowEmails(IntEnum):
class SocketSecurity(IntEnum):
"""Socket security"""
"""Socket security."""
AUTOMATIC = 0
SSL = 1
@@ -240,7 +247,7 @@ class SocketSecurity(IntEnum):
class VideochatType(IntEnum):
"""Video chat URL type"""
"""Video chat URL type."""
UNKNOWN = 0
BASICWEBRTC = 1
@@ -1,3 +1,5 @@
"""Contact module."""
from dataclasses import dataclass
from typing import TYPE_CHECKING
@@ -11,8 +13,7 @@ if TYPE_CHECKING:
@dataclass
class Contact:
"""
Contact API.
"""Contact API.
Essentially a wrapper for RPC, account ID and a contact ID.
"""
@@ -45,8 +46,9 @@ class Contact:
self._rpc.change_contact_name(self.account.id, self.id, name)
def get_encryption_info(self) -> str:
"""Get a multi-line encryption info, containing your fingerprint and
the fingerprint of the contact.
"""Get a multi-line encryption info.
Encryption info contains your fingerprint and the fingerprint of the contact.
"""
return self._rpc.get_contact_encryption_info(self.account.id, self.id)
@@ -66,4 +68,5 @@ class Contact:
)
def make_vcard(self) -> str:
"""Make a vCard for the contact."""
return self.account.make_vcard([self])
@@ -1,3 +1,5 @@
"""Account manager module."""
from __future__ import annotations
from typing import TYPE_CHECKING
@@ -10,12 +12,13 @@ if TYPE_CHECKING:
class DeltaChat:
"""
Delta Chat accounts manager.
"""Delta Chat accounts manager.
This is the root of the object oriented API.
"""
def __init__(self, rpc: "Rpc") -> None:
"""Initialize account manager."""
self.rpc = rpc
def add_account(self) -> Account:
@@ -37,9 +40,7 @@ class DeltaChat:
self.rpc.stop_io_for_all_accounts()
def maybe_network(self) -> None:
"""Indicate that the network likely has come back or just that the network
conditions might have changed.
"""
"""Indicate that the network conditions might have changed."""
self.rpc.maybe_network()
def get_system_info(self) -> AttrDict:
@@ -36,7 +36,7 @@ class EventFilter(ABC):
@abstractmethod
def __hash__(self) -> int:
"""Object's unique hash"""
"""Object's unique hash."""
@abstractmethod
def __eq__(self, other) -> bool:
@@ -52,9 +52,7 @@ class EventFilter(ABC):
@abstractmethod
def filter(self, event):
"""Return True-like value if the event passed the filter and should be
used, or False-like value otherwise.
"""
"""Return True-like value if the event passed the filter."""
class RawEvent(EventFilter):
@@ -82,31 +80,17 @@ class RawEvent(EventFilter):
return False
def filter(self, event: "AttrDict") -> bool:
"""Filter an event.
Return true if the event should be processed.
"""
if self.types and event.kind not in self.types:
return False
return self._call_func(event)
class NewMessage(EventFilter):
"""Matches whenever a new message arrives.
Warning: registering a handler for this event will cause the messages
to be marked as read. Its usage is mainly intended for bots.
:param pattern: if set, this Pattern will be used to filter the message by its text
content.
:param command: If set, only match messages with the given command (ex. /help).
Setting this property implies `is_info==False`.
:param is_bot: If set to True only match messages sent by bots, if set to None
match messages from bots and users. If omitted or set to False
only messages from users will be matched.
:param is_info: If set to True only match info/system messages, if set to False
only match messages that are not info/system messages. If omitted
info/system messages as well as normal messages will be matched.
:param func: A Callable function that should accept the event as input
parameter, and return a bool value indicating whether the event
should be dispatched or not.
"""
"""Matches whenever a new message arrives."""
def __init__(
self,
@@ -121,6 +105,25 @@ class NewMessage(EventFilter):
is_info: Optional[bool] = None,
func: Optional[Callable[["AttrDict"], bool]] = None,
) -> None:
"""Initialize a new message filter.
Warning: registering a handler for this event will cause the messages
to be marked as read. Its usage is mainly intended for bots.
:param pattern: if set, this Pattern will be used to filter the message by its text
content.
:param command: If set, only match messages with the given command (ex. /help).
Setting this property implies `is_info==False`.
:param is_bot: If set to True only match messages sent by bots, if set to None
match messages from bots and users. If omitted or set to False
only messages from users will be matched.
:param is_info: If set to True only match info/system messages, if set to False
only match messages that are not info/system messages. If omitted
info/system messages as well as normal messages will be matched.
:param func: A Callable function that should accept the event as input
parameter, and return a bool value indicating whether the event
should be dispatched or not.
"""
super().__init__(func=func)
self.is_bot = is_bot
self.is_info = is_info
@@ -159,6 +162,7 @@ class NewMessage(EventFilter):
return False
def filter(self, event: "AttrDict") -> bool:
"""Return true if if the event is a new message event."""
if self.is_bot is not None and self.is_bot != event.message_snapshot.is_bot:
return False
if self.is_info is not None and self.is_info != event.message_snapshot.is_info:
@@ -199,6 +203,7 @@ class MemberListChanged(EventFilter):
return False
def filter(self, event: "AttrDict") -> bool:
"""Return true if if the event is a member addition event."""
if self.added is not None and self.added != event.member_added:
return False
return self._call_func(event)
@@ -231,6 +236,7 @@ class GroupImageChanged(EventFilter):
return False
def filter(self, event: "AttrDict") -> bool:
"""Return True if event is matched."""
if self.deleted is not None and self.deleted != event.image_deleted:
return False
return self._call_func(event)
@@ -256,13 +262,12 @@ class GroupNameChanged(EventFilter):
return False
def filter(self, event: "AttrDict") -> bool:
"""Return True if event is matched."""
return self._call_func(event)
class HookCollection:
"""
Helper class to collect event hooks that can later be added to a Delta Chat client.
"""
"""Helper class to collect event hooks that can later be added to a Delta Chat client."""
def __init__(self) -> None:
self._hooks: set[tuple[Callable, Union[type, EventFilter]]] = set()
@@ -1,3 +1,5 @@
"""Message module."""
import json
from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional, Union
@@ -45,6 +47,7 @@ class Message:
return None
def get_sender_contact(self) -> Contact:
"""Return sender contact."""
from_id = self.get_snapshot().from_id
return self.account.get_contact_by_id(from_id)
@@ -53,6 +56,11 @@ class Message:
self._rpc.markseen_msgs(self.account.id, [self.id])
def continue_autocrypt_key_transfer(self, setup_code: str) -> None:
"""Continue the Autocrypt Setup Message key transfer.
This function can be called on received Autocrypt Setup Message
to import the key encrypted with the provided setup code.
"""
self._rpc.continue_autocrypt_key_transfer(self.account.id, self.id, setup_code)
def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
@@ -62,6 +70,7 @@ class Message:
self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description)
def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
"""Return a list of Webxdc status updates for Webxdc instance message."""
return json.loads(self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
def get_info(self) -> str:
@@ -69,6 +78,7 @@ class Message:
return self._rpc.get_message_info(self.account.id, self.id)
def get_webxdc_info(self) -> dict:
"""Get info from a Webxdc message in JSON format."""
return self._rpc.get_webxdc_info(self.account.id, self.id)
def wait_until_delivered(self) -> None:
@@ -80,8 +90,10 @@ class Message:
@futuremethod
def send_webxdc_realtime_advertisement(self):
"""Send an advertisement to join the realtime channel."""
yield self._rpc.send_webxdc_realtime_advertisement.future(self.account.id, self.id)
@futuremethod
def send_webxdc_realtime_data(self, data) -> None:
"""Send data to the realtime channel."""
yield self._rpc.send_webxdc_realtime_data.future(self.account.id, self.id, list(data))
@@ -1,3 +1,5 @@
"""Pytest plugin module."""
from __future__ import annotations
import os
@@ -13,24 +15,30 @@ from .rpc import Rpc
class ACFactory:
"""Test account factory."""
def __init__(self, deltachat: DeltaChat) -> None:
self.deltachat = deltachat
def get_unconfigured_account(self) -> Account:
"""Create a new unconfigured account."""
account = self.deltachat.add_account()
account.set_config("verified_one_on_one_chats", "1")
return account
def get_unconfigured_bot(self) -> Bot:
"""Create a new unconfigured bot."""
return Bot(self.get_unconfigured_account())
def get_credentials(self) -> (str, str):
"""Generate new credentials for chatmail account."""
domain = os.getenv("CHATMAIL_DOMAIN")
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
return f"{username}@{domain}", f"{username}${username}"
@futuremethod
def new_configured_account(self):
"""Create a new configured account."""
addr, password = self.get_credentials()
account = self.get_unconfigured_account()
params = {"addr": addr, "password": password}
@@ -40,6 +48,7 @@ class ACFactory:
return account
def new_configured_bot(self) -> Bot:
"""Create a new configured bot."""
addr, password = self.get_credentials()
bot = self.get_unconfigured_bot()
bot.configure(addr, password)
@@ -47,11 +56,13 @@ class ACFactory:
@futuremethod
def get_online_account(self):
"""Create a new account and start I/O."""
account = yield self.new_configured_account.future()
account.bring_online()
return account
def get_online_accounts(self, num: int) -> list[Account]:
"""Create multiple online accounts."""
futures = [self.get_online_account.future() for _ in range(num)]
return [f() for f in futures]
@@ -66,6 +77,10 @@ class ACFactory:
return ac_clone
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:
"""Create a new 1:1 chat between ac1 and ac2 accepted on both sides.
Returned chat is a chat with ac2 from ac1 point of view.
"""
ac2.create_chat(ac1)
return ac1.create_chat(ac2)
@@ -77,6 +92,7 @@ class ACFactory:
file: Optional[str] = None,
group: Optional[str] = None,
) -> Message:
"""Send a message."""
if not from_account:
from_account = (self.get_online_accounts(1))[0]
to_contact = from_account.create_contact(to_account)
@@ -95,6 +111,7 @@ class ACFactory:
file: Optional[str] = None,
group: Optional[str] = None,
) -> AttrDict:
"""Send a message and wait until recipient processes it."""
self.send_message(
to_account=to_client.account,
from_account=from_account,
@@ -108,6 +125,7 @@ class ACFactory:
@pytest.fixture
def rpc(tmp_path) -> AsyncGenerator:
"""RPC client fixture."""
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
with rpc_server:
yield rpc_server
@@ -115,6 +133,7 @@ def rpc(tmp_path) -> AsyncGenerator:
@pytest.fixture
def acfactory(rpc) -> AsyncGenerator:
"""Return account factory fixture."""
return ACFactory(DeltaChat(rpc))
@@ -132,7 +151,7 @@ def data():
raise Exception("Data path cannot be found")
def get_path(self, bn):
"""return path of file or None if it doesn't exist."""
"""Return path of file or None if it doesn't exist."""
fn = os.path.join(self.path, *bn.split("/"))
assert os.path.exists(fn)
return fn
@@ -1,3 +1,5 @@
"""JSON-RPC client module."""
from __future__ import annotations
import itertools
@@ -12,16 +14,19 @@ from typing import Any, Iterator, Optional
class JsonRpcError(Exception):
pass
"""JSON-RPC error."""
class RpcFuture:
"""RPC future waiting for RPC call result."""
def __init__(self, rpc: "Rpc", request_id: int, event: Event):
self.rpc = rpc
self.request_id = request_id
self.event = event
def __call__(self):
"""Wait for the future to return the result."""
self.event.wait()
response = self.rpc.request_results.pop(self.request_id)
if "error" in response:
@@ -32,17 +37,19 @@ class RpcFuture:
class RpcMethod:
"""RPC method."""
def __init__(self, rpc: "Rpc", name: str):
self.rpc = rpc
self.name = name
def __call__(self, *args) -> Any:
"""Synchronously calls JSON-RPC method."""
"""Call JSON-RPC method synchronously."""
future = self.future(*args)
return future()
def future(self, *args) -> Any:
"""Asynchronously calls JSON-RPC method."""
"""Call JSON-RPC method asynchronously."""
request_id = next(self.rpc.id_iterator)
request = {
"jsonrpc": "2.0",
@@ -58,8 +65,13 @@ class RpcMethod:
class Rpc:
"""RPC client."""
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
"""The given arguments will be passed to subprocess.Popen()"""
"""Initialize RPC client.
The given arguments will be passed to subprocess.Popen().
"""
if accounts_dir:
kwargs["env"] = {
**kwargs.get("env", os.environ),
@@ -81,6 +93,7 @@ class Rpc:
self.events_thread: Thread
def start(self) -> None:
"""Start RPC server subprocess."""
if sys.version_info >= (3, 11):
self.process = subprocess.Popen(
"deltachat-rpc-server",
@@ -130,6 +143,7 @@ class Rpc:
self.close()
def reader_loop(self) -> None:
"""Process JSON-RPC responses from the RPC server process output."""
try:
while line := self.process.stdout.readline():
response = json.loads(line)
@@ -157,12 +171,13 @@ class Rpc:
logging.exception("Exception in the writer loop")
def get_queue(self, account_id: int) -> Queue:
"""Get event queue corresponding to the given account ID."""
if account_id not in self.event_queues:
self.event_queues[account_id] = Queue()
return self.event_queues[account_id]
def events_loop(self) -> None:
"""Requests new events and distributes them between queues."""
"""Request new events and distributes them between queues."""
try:
while True:
if self.closing:
@@ -178,12 +193,12 @@ class Rpc:
logging.exception("Exception in the event loop")
def wait_for_event(self, account_id: int) -> Optional[dict]:
"""Waits for the next event from the given account and returns it."""
"""Wait for the next event from the given account and returns it."""
queue = self.get_queue(account_id)
return queue.get()
def clear_all_events(self, account_id: int):
"""Removes all queued-up events for a given account. Useful for tests."""
"""Remove all queued-up events for a given account. Useful for tests."""
queue = self.get_queue(account_id)
try:
while True:
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.159.3"
version = "1.160.0"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"
@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "1.159.3"
"version": "1.160.0"
}
+3 -3
View File
@@ -73,7 +73,7 @@ async fn main_impl() -> Result<()> {
.init();
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "accounts".to_string());
log::info!("Starting with accounts directory `{}`.", path);
log::info!("Starting with accounts directory `{path}`.");
let writable = true;
let accounts = Accounts::new(PathBuf::from(&path), writable).await?;
@@ -97,7 +97,7 @@ async fn main_impl() -> Result<()> {
Some(message) => serde_json::to_string(&message)?,
}
};
log::trace!("RPC send {}", message);
log::trace!("RPC send {message}");
println!("{message}");
}
Ok(())
@@ -141,7 +141,7 @@ async fn main_impl() -> Result<()> {
Some(message) => message,
}
};
log::trace!("RPC recv {}", message);
log::trace!("RPC recv {message}");
let session = session.clone();
tokio::spawn(async move {
session.handle_incoming(&message).await;
+9 -8
View File
@@ -10,9 +10,6 @@ ignore = [
# Unmaintained instant
"RUSTSEC-2024-0384",
# Unmaintained backoff
"RUSTSEC-2025-0012",
# Unmaintained paste
"RUSTSEC-2024-0436",
]
@@ -25,26 +22,29 @@ ignore = [
skip = [
{ name = "async-channel", version = "1.9.0" },
{ name = "bitflags", version = "1.3.2" },
{ name = "core-foundation", version = "0.9.4" },
{ 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" },
{ name = "hashbrown", version = "0.14.5" },
{ name = "heck", version = "0.4.1" },
{ name = "http", version = "0.2.12" },
{ name = "linux-raw-sys", version = "0.4.14" },
{ name = "loom", version = "0.5.6" },
{ name = "lru", version = "0.12.3" },
{ name = "netlink-packet-route", version = "0.17.1" },
{ name = "nix", version = "0.26.4" },
{ name = "nix", version = "0.27.1" },
{ name = "nom", version = "7.1.3" },
{ name = "rand_chacha", version = "0.3.1" },
{ name = "rand_core", version = "0.6.4" },
{ name = "rand", version = "0.8.5" },
{ name = "redox_syscall", version = "0.3.5" },
{ name = "redox_syscall", version = "0.4.1" },
{ name = "regex-automata", version = "0.1.10" },
{ name = "regex-syntax", version = "0.6.29" },
{ name = "rtnetlink", version = "0.13.1" },
{ name = "rustix", version = "0.38.44" },
{ name = "security-framework", version = "2.11.1" },
{ 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" },
@@ -84,6 +84,7 @@ allow = [
"MPL-2.0",
"Unicode-3.0",
"Unicode-DFS-2016",
"Unlicense",
"Zlib",
]
Generated
+12 -12
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": {
+1 -1
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 }
+5 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "1.159.3"
version = "1.160.0"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"
requires-python = ">=3.8"
@@ -47,6 +47,10 @@ line-length = 120
[tool.ruff]
lint.select = ["E", "F", "W", "YTT", "C4", "ISC", "ICN", "TID", "DTZ", "PLC", "PLE", "PLW", "PIE", "COM", "UP004", "UP010", "UP031", "UP032", "ANN204"]
lint.ignore = [
"PLC0415", # `import` should be at the top-level of a file
"PLW1641" # Object does not implement `__hash__` method
]
line-length = 120
[tool.isort]
-4
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)
+6 -4
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()
+2 -2
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
-1
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)
+1 -1
View File
@@ -1 +1 @@
2025-04-24
2025-06-22
+1 -1
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
+11 -14
View File
@@ -5,12 +5,10 @@ use std::future::Future;
use std::path::{Path, PathBuf};
use anyhow::{bail, ensure, Context as _, Result};
use futures::stream::FuturesUnordered;
use futures::StreamExt;
use serde::{Deserialize, Serialize};
use tokio::fs;
use tokio::io::AsyncWriteExt;
use tokio::task::JoinHandle;
use tokio::task::{JoinHandle, JoinSet};
use uuid::Uuid;
#[cfg(not(target_os = "ios"))]
@@ -20,6 +18,7 @@ use tokio::time::{sleep, Duration};
use crate::context::{Context, ContextBuilder};
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::log::{info, warn};
use crate::push::PushSubscriber;
use crate::stock_str::StockStrings;
@@ -304,12 +303,6 @@ impl Accounts {
/// This is an auxiliary function and not part of public API.
/// Use [Accounts::background_fetch] instead.
async fn background_fetch_no_timeout(accounts: Vec<Context>, events: Events) {
async fn background_fetch_and_log_error(account: Context) {
if let Err(error) = account.background_fetch().await {
warn!(account, "{error:#}");
}
}
events.emit(Event {
id: 0,
typ: EventType::Info(format!(
@@ -317,11 +310,15 @@ impl Accounts {
accounts.len()
)),
});
let mut futures_unordered: FuturesUnordered<_> = accounts
.into_iter()
.map(background_fetch_and_log_error)
.collect();
while futures_unordered.next().await.is_some() {}
let mut set = JoinSet::new();
for account in accounts {
set.spawn(async move {
if let Err(error) = account.background_fetch().await {
warn!(account, "{error:#}");
}
});
}
set.join_all().await;
}
/// Auxiliary function for [Accounts::background_fetch].
+2 -2
View File
@@ -20,7 +20,7 @@ use crate::config::Config;
use crate::constants::{self, MediaQuality};
use crate::context::Context;
use crate::events::EventType;
use crate::log::LogExt;
use crate::log::{error, info, warn, LogExt};
use crate::tools::sanitize_filename;
/// Represents a file in the blob directory.
@@ -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}")
};
+78 -38
View File
@@ -34,7 +34,7 @@ use crate::download::DownloadState;
use crate::ephemeral::{start_chat_ephemeral_timers, Timer as EphemeralTimer};
use crate::events::EventType;
use crate::location;
use crate::log::LogExt;
use crate::log::{error, info, warn, LogExt};
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
use crate::mimefactory::MimeFactory;
use crate::mimeparser::SystemMessage;
@@ -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,
@@ -995,6 +995,7 @@ impl ChatId {
transaction.execute(
"INSERT INTO msgs (
chat_id,
rfc724_mid,
from_id,
timestamp,
type,
@@ -1004,9 +1005,10 @@ impl ChatId {
param,
hidden,
mime_in_reply_to)
VALUES (?,?,?,?,?,?,?,?,?,?);",
VALUES (?,?,?,?,?,?,?,?,?,?,?);",
(
self,
&msg.rfc724_mid,
ContactId::SELF,
time(),
msg.viewtype,
@@ -1506,7 +1508,7 @@ impl std::fmt::Display for ChatId {
/// This allows you to directly store [ChatId] into the database as
/// well as query for a [ChatId].
impl rusqlite::types::ToSql for ChatId {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
let val = rusqlite::types::Value::Integer(i64::from(self.0));
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
@@ -2378,7 +2380,7 @@ pub enum ChatVisibility {
}
impl rusqlite::types::ToSql for ChatVisibility {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
let val = rusqlite::types::Value::Integer(*self as i64);
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
@@ -2941,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
);
}
}
}
@@ -3532,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)
}
@@ -3996,7 +4026,7 @@ pub enum MuteDuration {
}
impl rusqlite::types::ToSql for MuteDuration {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
let duration: i64 = match &self {
MuteDuration::NotMuted => 0,
MuteDuration::Forever => -1,
@@ -4313,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();
@@ -4368,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
@@ -4389,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);
@@ -4431,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);
+92 -1
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.
+2 -1
View File
@@ -10,6 +10,7 @@ use crate::constants::{
};
use crate::contact::{Contact, ContactId};
use crate::context::Context;
use crate::log::warn;
use crate::message::{Message, MessageState, MsgId};
use crate::param::{Param, Params};
use crate::stock_str;
@@ -322,7 +323,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 })
+1 -1
View File
@@ -17,7 +17,7 @@ use crate::configure::EnteredLoginParam;
use crate::constants;
use crate::context::Context;
use crate::events::EventType;
use crate::log::LogExt;
use crate::log::{info, LogExt};
use crate::login_param::ConfiguredLoginParam;
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::provider::{get_provider_by_id, Provider};
+1 -1
View File
@@ -27,7 +27,7 @@ use crate::config::{self, Config};
use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT;
use crate::context::Context;
use crate::imap::Imap;
use crate::log::LogExt;
use crate::log::{info, warn, LogExt};
pub use crate::login_param::EnteredLoginParam;
use crate::login_param::{
ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
+1
View File
@@ -9,6 +9,7 @@ use quick_xml::events::{BytesStart, Event};
use super::{Error, ServerParams};
use crate::context::Context;
use crate::log::warn;
use crate::net::read_url;
use crate::provider::{Protocol, Socket};
+1
View File
@@ -9,6 +9,7 @@ use quick_xml::events::Event;
use super::{Error, ServerParams};
use crate::context::Context;
use crate::log::warn;
use crate::net::read_url;
use crate::provider::{Protocol, Socket};
-1
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
+15 -10
View File
@@ -25,11 +25,11 @@ 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};
use crate::log::LogExt;
use crate::log::{info, warn, LogExt};
use crate::message::MessageState;
use crate::mimeparser::AvatarAction;
use crate::param::{Param, Params};
@@ -243,7 +243,7 @@ impl fmt::Display for ContactId {
/// Allow converting [`ContactId`] to an SQLite type.
impl rusqlite::types::ToSql for ContactId {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
let val = rusqlite::types::Value::Integer(i64::from(self.0));
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
@@ -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)?;
+7
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(())
+8 -10
View File
@@ -10,8 +10,8 @@ use std::time::Duration;
use anyhow::{bail, ensure, Context as _, Result};
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};
@@ -28,6 +28,7 @@ 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::log::{info, warn};
use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam};
use crate::message::{self, Message, MessageState, MsgId};
use crate::param::{Param, Params};
@@ -773,13 +774,11 @@ impl Context {
/// Returns information about the context as key-value pairs.
pub async fn get_info(&self) -> Result<BTreeMap<&'static str, String>> {
let unset = "0";
let l = EnteredLoginParam::load(self).await?;
let l2 = ConfiguredLoginParam::load(self)
.await?
.map_or_else(|| "Not configured".to_string(), |param| param.to_string());
let secondary_addrs = self.get_secondary_self_addrs().await?.join(", ");
let displayname = self.get_config(Config::Displayname).await?;
let chats = get_chat_cnt(self).await?;
let unblocked_msgs = message::get_unblocked_msg_cnt(self).await;
let request_msgs = message::get_request_msg_cnt(self).await;
@@ -858,7 +857,6 @@ impl Context {
);
res.insert("journal_mode", journal_mode);
res.insert("blobdir", self.get_blobdir().display().to_string());
res.insert("displayname", displayname.unwrap_or_else(|| unset.into()));
res.insert(
"selfavatar",
self.get_config(Config::Selfavatar)
@@ -1061,21 +1059,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 +1153,7 @@ impl Context {
id
}
};
res += &format!("self_reporting_id {}", self_reporting_id);
res += &format!("self_reporting_id {self_reporting_id}");
Ok(res)
}
+7
View File
@@ -310,6 +310,13 @@ async fn test_get_info_completeness() {
"'{key}' missing in get_info() output"
);
}
if skip_from_get_info.contains(&&*key) {
assert!(
!info.contains_key(&*key),
"'{key}' should not be in get_info() output"
);
}
}
}
+1
View File
@@ -3,6 +3,7 @@ use crate::chat::ChatId;
use crate::config::Config;
use crate::context::Context;
use crate::events::EventType;
use crate::log::{error, info};
use crate::message::{Message, MsgId, Viewtype};
use crate::param::Param;
use crate::tools::time;
+5 -4
View File
@@ -9,16 +9,17 @@ use mailparse::ParsedMail;
use crate::aheader::Aheader;
use crate::context::Context;
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
use crate::log::info;
use crate::peerstate::Peerstate;
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);
};
+1
View File
@@ -10,6 +10,7 @@ use serde::{Deserialize, Serialize};
use crate::config::Config;
use crate::context::Context;
use crate::imap::session::Session;
use crate::log::info;
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::{MimeMessage, Part};
use crate::tools::time;
+4 -4
View File
@@ -11,6 +11,7 @@ use crate::aheader::{Aheader, EncryptPreference};
use crate::config::Config;
use crate::context::Context;
use crate::key::{load_self_public_key, load_self_secret_key, SignedPublicKey};
use crate::log::warn;
use crate::peerstate::Peerstate;
use crate::pgp;
@@ -143,7 +144,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 +154,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)
}
}
+2 -2
View File
@@ -81,7 +81,7 @@ use crate::context::Context;
use crate::download::MIN_DELETE_SERVER_AFTER;
use crate::events::EventType;
use crate::location;
use crate::log::LogExt;
use crate::log::{error, info, warn, LogExt};
use crate::message::{Message, MessageState, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::stock_str;
@@ -146,7 +146,7 @@ impl FromStr for Timer {
}
impl rusqlite::types::ToSql for Timer {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
let val = rusqlite::types::Value::Integer(match self {
Self::Disabled => 0,
Self::Enabled { duration } => i64::from(*duration),
+2 -2
View File
@@ -134,14 +134,14 @@ pub trait HeaderDefMap {
fn get_header_value(&self, headerdef: HeaderDef) -> Option<String>;
/// Returns requested header if it exists.
fn get_header(&self, headerdef: HeaderDef) -> Option<&MailHeader>;
fn get_header(&self, headerdef: HeaderDef) -> Option<&MailHeader<'_>>;
}
impl HeaderDefMap for [MailHeader<'_>] {
fn get_header_value(&self, headerdef: HeaderDef) -> Option<String> {
self.get_first_value(headerdef.get_headername())
}
fn get_header(&self, headerdef: HeaderDef) -> Option<&MailHeader> {
fn get_header(&self, headerdef: HeaderDef) -> Option<&MailHeader<'_>> {
self.get_first_header(headerdef.get_headername())
}
}
+1
View File
@@ -16,6 +16,7 @@ use mime::Mime;
use crate::context::Context;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::log::warn;
use crate::message::{self, Message, MsgId};
use crate::mimeparser::parse_message_id;
use crate::param::Param::SendHtml;
+9 -11
View File
@@ -32,7 +32,7 @@ use crate::contact::{Contact, ContactId, Modifier, Origin};
use crate::context::Context;
use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::log::LogExt;
use crate::log::{error, info, warn, LogExt};
use crate::login_param::{
prioritize_server_login_params, ConfiguredLoginParam, ConfiguredServerLoginParam,
};
@@ -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(())
@@ -2165,7 +2163,7 @@ pub(crate) fn get_folder_meaning(folder: &Name) -> FolderMeaning {
}
/// Parses the headers from the FETCH result.
fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader>> {
fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader<'_>>> {
match prefetch_msg.header() {
Some(header_bytes) => {
let (headers, _) = mailparse::parse_headers(header_bytes)?;
+1
View File
@@ -8,6 +8,7 @@ use tokio::io::BufWriter;
use super::capabilities::Capabilities;
use crate::context::Context;
use crate::log::{info, warn};
use crate::login_param::{ConnectionCandidate, ConnectionSecurity};
use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp};
use crate::net::proxy::ProxyConfig;
+1
View File
@@ -8,6 +8,7 @@ use tokio::time::timeout;
use super::session::Session;
use super::Imap;
use crate::context::Context;
use crate::log::{info, warn};
use crate::net::TIMEOUT;
use crate::tools::{self, time_elapsed};
+1 -2
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();
+17 -10
View File
@@ -5,7 +5,7 @@ use anyhow::{Context as _, Result};
use super::{get_folder_meaning_by_attrs, get_folder_meaning_by_name};
use crate::config::Config;
use crate::imap::{session::Session, Imap};
use crate::log::LogExt;
use crate::log::{info, LogExt};
use crate::tools::{self, time_elapsed};
use crate::{context::Context, imap::FolderMeaning};
@@ -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)
}
}
+10 -8
View File
@@ -5,6 +5,7 @@ use anyhow::Context as _;
use super::session::Session as ImapSession;
use super::{get_uid_next, get_uidvalidity, set_modseq, set_uid_next, set_uidvalidity};
use crate::context::Context;
use crate::log::{info, warn};
type Result<T> = std::result::Result<T, Error>;
@@ -33,10 +34,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 +74,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 +96,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 +131,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 +139,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 +147,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
+16 -5
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;
@@ -18,20 +17,21 @@ use crate::blob::BlobDirContents;
use crate::chat::delete_and_reset_all_device_msgs;
use crate::config::Config;
use crate::context::Context;
use crate::e2ee;
use crate::events::EventType;
use crate::key::{self, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey};
use crate::log::LogExt;
use crate::log::{error, info, warn, LogExt};
use crate::pgp;
use crate::qr::DCBACKUP_VERSION;
use crate::sql;
use crate::tools::{
create_folder, delete_file, get_filesuffix_lc, read_file, time, write_file, TempPathGuard,
};
use crate::{e2ee, stock_str};
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};
@@ -102,7 +102,14 @@ pub async fn imex(
if let Err(err) = res.as_ref() {
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
error!(context, "IMEX failed to complete: {:#}", err);
if err
.to_string()
.contains("Backup too new, please update Delta Chat")
{
error!(context, "{}", stock_str::old_backup(context));
} else {
error!(context, "IMEX failed to complete: {:#}", err);
}
context.emit_event(EventType::ImexProgress(0));
} else {
info!(context, "IMEX successfully completed");
@@ -173,7 +180,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(())
}
+15 -15
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)]
+7 -1
View File
@@ -41,6 +41,7 @@ use tokio_util::sync::CancellationToken;
use crate::chat::add_device_msg;
use crate::context::Context;
use crate::imex::BlobDirContents;
use crate::log::{info, warn};
use crate::message::Message;
use crate::qr::Qr;
use crate::stock_str::backup_transfer_msg_body;
@@ -95,6 +96,7 @@ impl BackupProvider {
pub async fn prepare(context: &Context) -> Result<Self> {
let relay_mode = RelayMode::Disabled;
let endpoint = Endpoint::builder()
.tls_x509() // For compatibility with iroh <0.34.0
.alpns(vec![BACKUP_ALPN.to_vec()])
.relay_mode(relay_mode)
.bind()
@@ -301,7 +303,11 @@ pub async fn get_backup2(
) -> Result<()> {
let relay_mode = RelayMode::Disabled;
let endpoint = Endpoint::builder().relay_mode(relay_mode).bind().await?;
let endpoint = Endpoint::builder()
.tls_x509() // For compatibility with iroh <0.34.0
.relay_mode(relay_mode)
.bind()
.await?;
let conn = endpoint.connect(node_addr, BACKUP_ALPN).await?;
let (mut send_stream, mut recv_stream) = conn.open_bi().await?;
+29 -9
View File
@@ -10,12 +10,12 @@ 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;
use crate::context::Context;
use crate::log::LogExt;
use crate::log::{info, LogExt};
use crate::pgp::KeyPair;
use crate::tools::{self, time_elapsed};
@@ -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)
}
}
+1
View File
@@ -22,6 +22,7 @@ use crate::constants::DC_CHAT_ID_TRASH;
use crate::contact::ContactId;
use crate::context::Context;
use crate::events::EventType;
use crate::log::{info, warn};
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::tools::{duration_to_str, time};
+26 -15
View File
@@ -4,7 +4,6 @@
use crate::context::Context;
#[macro_export]
macro_rules! info {
($ctx:expr, $msg:expr) => {
info!($ctx, $msg,)
@@ -19,22 +18,30 @@ macro_rules! info {
}};
}
#[macro_export]
macro_rules! warn {
($ctx:expr, $msg:expr) => {
warn!($ctx, $msg,)
};
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
let formatted = format!($msg, $($args),*);
let full = format!("{file}:{line}: {msg}",
file = file!(),
line = line!(),
msg = &formatted);
$ctx.emit_event($crate::EventType::Warning(full));
}};
pub(crate) use info;
// Workaround for <https://github.com/rust-lang/rust/issues/133708>.
#[macro_use]
mod warn_macro_mod {
macro_rules! warn_macro {
($ctx:expr, $msg:expr) => {
warn_macro!($ctx, $msg,)
};
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
let formatted = format!($msg, $($args),*);
let full = format!("{file}:{line}: {msg}",
file = file!(),
line = line!(),
msg = &formatted);
$ctx.emit_event($crate::EventType::Warning(full));
}};
}
pub(crate) use warn_macro;
}
#[macro_export]
pub(crate) use warn_macro_mod::warn_macro as warn;
macro_rules! error {
($ctx:expr, $msg:expr) => {
error!($ctx, $msg,)
@@ -46,6 +53,8 @@ macro_rules! error {
}};
}
pub(crate) use error;
impl Context {
/// Set last error string.
/// Implemented as blocking as used from macros in different, not always async blocks.
@@ -102,6 +111,8 @@ impl<T, E: std::fmt::Display> LogExt<T, E> for Result<T, E> {
#[cfg(test)]
mod tests {
use super::*;
use anyhow::Result;
use crate::test_utils::TestContext;
+14 -13
View File
@@ -809,20 +809,14 @@ impl ConfiguredLoginParam {
entered_param: &EnteredLoginParam,
) -> Result<()> {
let addr = addr_normalize(&self.addr);
let provider_id = self.provider.map(|provider| provider.id);
let configured_addr = context.get_config(Config::ConfiguredAddr).await?;
if let Some(configured_addr) = configured_addr {
if let Some(configured_addr) = &configured_addr {
ensure!(
addr_cmp(&configured_addr, &addr,),
addr_cmp(configured_addr, &addr),
"Adding a second transport is not supported right now."
);
}
context
.sql
.set_raw_config(
Config::ConfiguredProvider.as_ref(),
self.provider.map(|provider| provider.id),
)
.await?;
context
.sql
.execute(
@@ -837,10 +831,17 @@ impl ConfiguredLoginParam {
),
)
.await?;
context
.sql
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr))
.await?;
if configured_addr.is_none() {
// If there is no transport yet, set the new transport as the primary one
context
.sql
.set_raw_config(Config::ConfiguredProvider.as_ref(), provider_id)
.await?;
context
.sql
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr))
.await?;
}
Ok(())
}

Some files were not shown because too many files have changed in this diff Show More