Compare commits

...
Author SHA1 Message Date
Floris Bruynooghe feb21b8731 Start refactoring peer channels to stop iroh when not needed 2025-07-16 14:42:54 +02:00
link2xt a8a7cec376 refactor: use CancellationToken instead of a 1-message channel 2025-06-11 14:28:24 +00:00
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
Sebastian Klähn 846c8e7f1b Generate rfc724_mid when creating Message (#6704)
Set `rfc724_mid` in `Message::new()`, `Message::new_text()`, and
`Message::default()` instead of when sending the message. This way the
rfc724 mid can be read in the draft stage which makes it more consistent
for bots. Tests had to be adjusted to create multiple messages to get
unique mid, otherwise core would not send the messages out.
2025-05-05 15:06:05 +00:00
iequidoo 98a1b9e373 test: Profile data is attached to group leave messages 2025-05-05 05:28:43 -03:00
dependabot[bot] ba55dd339e Merge pull request #6842 from chatmail/dependabot/cargo/chrono-0.4.41 2025-05-03 02:55:28 +00:00
dependabot[bot] 5a2ce60392 chore(cargo): bump chrono from 0.4.40 to 0.4.41
Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.40 to 0.4.41.
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chronotope/chrono/compare/v0.4.40...v0.4.41)

---
updated-dependencies:
- dependency-name: chrono
  dependency-version: 0.4.41
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-02 23:36:52 +00:00
dependabot[bot] 7ebcee14e7 Merge pull request #6839 from chatmail/dependabot/cargo/data-encoding-2.9.0 2025-05-02 23:35:05 +00:00
dependabot[bot] ccf829fe8c Merge pull request #6837 from chatmail/dependabot/cargo/syn-2.0.101 2025-05-02 23:29:57 +00:00
dependabot[bot] a274f5fb86 Merge pull request #6832 from chatmail/dependabot/cargo/anyhow-1.0.98 2025-05-02 23:29:42 +00:00
dependabot[bot] 5421a555f4 Merge pull request #6840 from chatmail/dependabot/cargo/sha2-0.10.9 2025-05-02 23:29:25 +00:00
dependabot[bot] b1233b2b07 chore(cargo): bump anyhow from 1.0.97 to 1.0.98
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.97 to 1.0.98.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.97...1.0.98)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-version: 1.0.98
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-02 16:22:22 +00:00
dependabot[bot] e55ac59846 chore(cargo): bump sha2 from 0.10.8 to 0.10.9
Bumps [sha2](https://github.com/RustCrypto/hashes) from 0.10.8 to 0.10.9.
- [Commits](https://github.com/RustCrypto/hashes/compare/sha2-v0.10.8...sha2-v0.10.9)

---
updated-dependencies:
- dependency-name: sha2
  dependency-version: 0.10.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-02 16:15:55 +00:00
dependabot[bot] cd6cd6ba47 chore(cargo): bump data-encoding from 2.8.0 to 2.9.0
Bumps [data-encoding](https://github.com/ia0/data-encoding) from 2.8.0 to 2.9.0.
- [Commits](https://github.com/ia0/data-encoding/compare/v2.8.0...v2.9.0)

---
updated-dependencies:
- dependency-name: data-encoding
  dependency-version: 2.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-02 16:15:04 +00:00
dependabot[bot] 026b06003b chore(cargo): bump syn from 2.0.100 to 2.0.101
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.100 to 2.0.101.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.100...2.0.101)

---
updated-dependencies:
- dependency-name: syn
  dependency-version: 2.0.101
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-02 16:06:28 +00:00
dependabot[bot] 02141b86c2 Merge pull request #6834 from chatmail/dependabot/cargo/quick-xml-0.37.5 2025-05-02 16:05:37 +00:00
dependabot[bot] 9bb2600d73 Merge pull request #6836 from chatmail/dependabot/cargo/libc-0.2.172 2025-05-02 16:05:23 +00:00
dependabot[bot] 33ea13daf4 Merge pull request #6829 from chatmail/dependabot/cargo/blake3-1.8.2 2025-05-02 16:04:56 +00:00
dependabot[bot] 10b6019e7e Merge pull request #6831 from chatmail/dependabot/cargo/smallvec-1.15.0 2025-05-02 16:03:13 +00:00
dependabot[bot] 727f0ab6ce Merge pull request #6841 from chatmail/dependabot/cargo/brotli-8.0.0 2025-05-02 16:02:50 +00:00
dependabot[bot] 31752e9674 chore(cargo): bump brotli from 7.0.0 to 8.0.0
Bumps [brotli](https://github.com/dropbox/rust-brotli) from 7.0.0 to 8.0.0.
- [Release notes](https://github.com/dropbox/rust-brotli/releases)
- [Commits](https://github.com/dropbox/rust-brotli/compare/7.0.0...8.0.0)

---
updated-dependencies:
- dependency-name: brotli
  dependency-version: 8.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-01 21:10:36 +00:00
dependabot[bot] 841d4e6e1e chore(cargo): bump libc from 0.2.171 to 0.2.172
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.171 to 0.2.172.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.172/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.171...0.2.172)

---
updated-dependencies:
- dependency-name: libc
  dependency-version: 0.2.172
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-01 21:09:03 +00:00
dependabot[bot] 7dc890119d chore(cargo): bump quick-xml from 0.37.4 to 0.37.5
Bumps [quick-xml](https://github.com/tafia/quick-xml) from 0.37.4 to 0.37.5.
- [Release notes](https://github.com/tafia/quick-xml/releases)
- [Changelog](https://github.com/tafia/quick-xml/blob/master/Changelog.md)
- [Commits](https://github.com/tafia/quick-xml/compare/v0.37.4...v0.37.5)

---
updated-dependencies:
- dependency-name: quick-xml
  dependency-version: 0.37.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-01 21:08:21 +00:00
dependabot[bot] 293a683484 chore(cargo): bump smallvec from 1.14.0 to 1.15.0
Bumps [smallvec](https://github.com/servo/rust-smallvec) from 1.14.0 to 1.15.0.
- [Release notes](https://github.com/servo/rust-smallvec/releases)
- [Commits](https://github.com/servo/rust-smallvec/compare/v1.14.0...v1.15.0)

---
updated-dependencies:
- dependency-name: smallvec
  dependency-version: 1.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-01 21:07:14 +00:00
dependabot[bot] 737bc15382 chore(cargo): bump blake3 from 1.8.0 to 1.8.2
Bumps [blake3](https://github.com/BLAKE3-team/BLAKE3) from 1.8.0 to 1.8.2.
- [Release notes](https://github.com/BLAKE3-team/BLAKE3/releases)
- [Commits](https://github.com/BLAKE3-team/BLAKE3/compare/1.8.0...1.8.2)

---
updated-dependencies:
- dependency-name: blake3
  dependency-version: 1.8.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-01 21:06:40 +00:00
B. Petersen 1a72711999 chore: adapt some top-level-mentions of delta 2025-04-30 01:07:09 +02:00
bjoernandiequidoo 3fea829340 feat: better avatar quality (#6822)
this PR scaled avatars using the Triangle-filter,
resulting in often better image quality and smaller files (5%).

it comes at high costs,
therefore, we do not do that unconditionally for each image sent, see
comment in the code
and https://github.com/chatmail/core/pull/6815

---------

Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com>
2025-04-24 18:44:23 +00:00
B. Petersen 6dba14158a fix: emit progress(0) in case AEAP is tried 2025-04-24 18:32:29 +02:00
link2xt 83bc497f0d chore(release): prepare for 1.159.3 2025-04-24 13:44:06 +00:00
link2xt 990a13fd96 ci: use ubuntu-latest for @deltachat/jsonrpc-client publishing
Ubuntu 20.04 runner is removed.
2025-04-24 13:39:50 +00:00
link2xt 29b84424f4 chore(release): prepare for 1.159.2 2025-04-23 23:08:07 +00:00
Hocuri ef798cd86d fix: Allow to send to chats after failed securejoin again (#6817)
Revert the biggest part of https://github.com/chatmail/core/pull/6722/
in order to fix #6816. Reopens
https://github.com/chatmail/core/issues/6706.

Rationale for reverting instead of fixing is that it's not trivial to
implement "if the chat is encrypted, can_send() returns true": When
sending a message, in order to check whether to encrypt, we load all
peerstates and check whether all of them can be encrypted to
(`should_encrypt()`). We could do this in `can_send()`, but this would
make it quite slow for groups. With multi-transport, the ways of
checking whether to encrypt will be different, so in order not to do
unnecessary work now, this PR just revert parts of
[https://github.com/chatmail/core/pull/6722/](https://github.com/chatmail/core/pull/6817#),
so that we can make things work nicely when multi-transport is merged.

As a quick mitigation, we could increase the timeout from 15s to
something like 1 minute or 1 day: Long enough that usually securejoin
will finish before, but short enough that it's possible to send to old
chats that had a failed securejoin long in the past.
2025-04-23 20:55:50 +00:00
WofWca 9d3450f50c chore: clean up deltachat-jsonrpc dependencies
Move the dev dependencies to `[dev-dependencies]`,
remove the unused `log` dependency.
2025-04-19 15:57:43 +04:00
Hocuri 1db9b77711 fix: Lowercase address in add_transport() (#6805) 2025-04-17 12:19:28 +00:00
Mark Felder a6713630b9 update 'takes longer' fallback wording again 2025-04-17 11:00:56 +02:00
link2xt 4168985869 chore: update yerpc to 0.6.4 2025-04-16 22:57:07 +00:00
link2xt 1ea8647018 test: test that key of the recipient is gossiped in 1:1 chats
It is needed for multi-device setups.
2025-04-16 12:00:47 +00:00
Hocuri f311cae5ad fix: Parse login scheme in add_transport_from_qr() (#6802)
fix https://github.com/chatmail/core/issues/6801
2025-04-15 10:23:49 +02:00
Hocuri 7e8e4d2f39 api: Rename add_transport() -> add_or_update_transport() (#6800)
cc @nicodh
2025-04-15 10:19:25 +02:00
Hocuriandl 1379821b03 refactor: Move logins into SQL table (#6724)
Move all `configured_*` parameters into a new SQL table `transports`.
All `configured_*` parameters are deprecated; the only exception is
`configured_addr`, which is used to store the address of the primary
transport. Currently, there can only ever be one primary transport (i.e.
the `transports` table only ever has one row); this PR is not supposed
to change DC's behavior in any meaningful way.

This is a preparation for mt.

---------

Co-authored-by: l <link2xt@testrun.org>
2025-04-13 19:06:41 +02:00
link2xt 1722cb8851 test: fix mismatch between the contact and the account in securejoin tests 2025-04-13 05:48:58 +00:00
iequidoo 49c300d2ac test: Check headers absense straightforwardly
In the `test` cfg, introduce `MimeMessage::headers_removed` hash set and `header_exists()` function
returning whether the header exists in any part of the parsed message. `get_header()` shouldn't be
used in tests for checking absense of headers because it returns `None` for removed ("ignored")
headers.
2025-04-12 23:24:54 -03:00
link2xt 0e3277bc5a chore(release): prepare for 1.159.1 2025-04-12 03:13:35 +00:00
link2xt 9f5e608c61 feat: track gossiping per (chat, fingerprint) pair
This change simplifies
updating the gossip timestamps
when we receive a message
because we only need to know
the keys received in Autocrypt-Gossip
header and which chat the message is
assigned to.
We no longer need to iterate
over the member list.

This is a preparation
for PGP contacts
and member lists that contain
key fingerprints rather than
email addresses.

This change also removes encryption preference
from Autocrypt-Gossip header.
It SHOULD NOT be gossiped
according to the Autocrypt specification
and we ignore encryption preference anyway
since 1.157.0.

test_gossip_optimization is removed
because it relied on a per-chat gossip_timestamp.
2025-04-12 02:51:11 +00:00
link2xt 0b82b42128 build: increase MSRV to 1.82.0
This allows using Option::is_none_or()
which is only available since 1.82.0.
2025-04-12 02:51:11 +00:00
link2xt b4828c251f docs: MimeFactory.member_timestamps has the same order as To: rather than RCPT TO: 2025-04-11 18:35:56 +00:00
link2xt 7a4f0eed23 test: encrypt test_remove_member_bcc 2025-04-11 14:02:09 +00:00
link2xt 54a6b0efcb test: encrypt test_subject_in_group() 2025-04-11 14:02:09 +00:00
iequidoo 9229eae4e0 test: Autocrypt-Gossip header isn't sent in broadcast messages
Follow-up to 175145969c.
2025-04-11 00:39:32 -03:00
link2xt 3e8987b460 test: port test_delete_deltachat_folder to JSON-RPC 2025-04-09 13:28:26 +00:00
link2xt 634cbd14f0 fix: restart I/O when mvbox_move setting is changed
When the setting is enabled,
new IMAP loop should be started.
2025-04-08 23:33:31 +00:00
link2xt 31cf663f8b api(deltachat-rpc-client): add Account.add_transport() 2025-04-08 21:51:54 +00:00
link2xt 175145969c fix: never send Autocrypt-Gossip in broadcast lists
Broadcast lists are encrypted since 1.159.0,
but Autocrypt-Gossip was not disabled.
As Autocrypt-Gossip contains the email address
and the key of the recipient, it should
not be sent to broadcast lists.
2025-04-08 21:50:25 +00:00
link2xt 8db1a01d9a build: update crossbeam-channel from 0.5.14 to 0.5.15
crossbeam-channel 0.5.14 is yanked.
2025-04-08 21:45:12 +00:00
l b3c5f64315 Merge pull request #6770 from chatmail/missing-chat-deleted-event
improve jsonrpc python bindings
2025-04-08 21:40:49 +00:00
Hocuri 35e717dd49 feat: Improve error message when the user tries to do AEAP (#6786)
The old error message was too confusing.
2025-04-08 23:37:35 +02:00
Hocuri 203e668928 ci: Don't make ruff format quiet (#6785)
Before this PR, it was not possible to see why `ruff format` failed
2025-04-08 15:11:59 +02:00
Hocuri de38b413f1 doc: Two JsonRPC doc improvements (#6778) 2025-04-08 14:50:12 +02:00
B. Petersen 21010f4de6 add jsonrpc for info_contact_id 2025-04-08 14:29:58 +02:00
link2xt b03edabb11 chore(release): perpare for 1.159.0 2025-04-08 02:23:33 +00:00
l 4001d79e4b ci: upgrade Rust from 1.84.1 to 1.86.0 (#6784) 2025-04-07 21:42:10 +00:00
missytake 3513a97a3d fix: ruff complains about import sorting 2025-04-07 23:37:44 +02:00
missytake 072855daef fix: syntax 2025-04-07 22:55:09 +02:00
dependabot[bot] dc87ba87c9 Merge pull request #6754 from chatmail/dependabot/cargo/serde-1.0.219 2025-04-07 20:11:20 +00:00
dependabot[bot] 2b3f030d6a chore(cargo): bump serde from 1.0.218 to 1.0.219
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.218 to 1.0.219.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.218...v1.0.219)

---
updated-dependencies:
- dependency-name: serde
  dependency-version: 1.0.219
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 19:51:39 +00:00
dependabot[bot] a3bbdf0bec Merge pull request #6751 from chatmail/dependabot/cargo/image-0.25.6 2025-04-07 19:50:34 +00:00
dependabot[bot] 7a7f95f5ef Merge pull request #6739 from chatmail/dependabot/cargo/tempfile-3.19.1 2025-04-07 19:50:14 +00:00
link2xt 5210b37601 test: update blob hash in blob::blob_tests::test_selfavatar_outside_blobdir 2025-04-07 18:40:09 +00:00
dependabot[bot] 4ad9fa144d chore(cargo): bump tempfile from 3.14.0 to 3.19.1
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.14.0 to 3.19.1.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.14.0...v3.19.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 18:22:12 +00:00
missytake 211badee41 feat: pass email and password via env in python-jsonrpc 2025-04-06 00:48:24 +02:00
missytake ba15591c22 fix: add missing ChatDeleted event to python jsonrpc client 2025-04-05 18:26:22 +02:00
dependabot[bot] c26e43630c chore(cargo): bump image from 0.25.5 to 0.25.6
Bumps [image](https://github.com/image-rs/image) from 0.25.5 to 0.25.6.
- [Changelog](https://github.com/image-rs/image/blob/main/CHANGES.md)
- [Commits](https://github.com/image-rs/image/compare/v0.25.5...v0.25.6)

---
updated-dependencies:
- dependency-name: image
  dependency-version: 0.25.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-02 18:16:07 +00:00
119 changed files with 4313 additions and 2319 deletions
+25 -10
View File
@@ -20,20 +20,24 @@ permissions: {}
env:
RUSTFLAGS: -Dwarnings
RUST_VERSION: 1.87.0
# Minimum Supported Rust Version
MSRV: 1.85.0
jobs:
lint_rust:
name: Lint Rust
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: 1.84.1
steps:
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false
- name: Install rustfmt and clippy
run: rustup toolchain install $RUSTUP_TOOLCHAIN --profile minimal --component rustfmt --component 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
@@ -91,25 +95,36 @@ jobs:
matrix:
include:
- os: ubuntu-latest
rust: 1.84.1
rust: latest
- os: windows-latest
rust: 1.84.1
rust: latest
- os: macos-latest
rust: 1.84.1
rust: latest
# Minimum Supported Rust Version = 1.81.0
# Minimum Supported Rust Version
- os: ubuntu-latest
rust: 1.81.0
rust: minimum
runs-on: ${{ matrix.os }}
steps:
- run:
echo "RUSTUP_TOOLCHAIN=$MSRV" >> $GITHUB_ENV
shell: bash
if: matrix.rust == 'minimum'
- run:
echo "RUSTUP_TOOLCHAIN=$RUST_VERSION" >> $GITHUB_ENV
shell: bash
if: matrix.rust == 'latest'
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false
- name: Install Rust ${{ matrix.rust }}
run: rustup toolchain install --profile minimal ${{ matrix.rust }}
- run: rustup override set ${{ matrix.rust }}
run: rustup toolchain install --profile minimal $RUSTUP_TOOLCHAIN
shell: bash
- run: rustup override set $RUSTUP_TOOLCHAIN
shell: bash
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2
+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
@@ -9,7 +9,7 @@ permissions: {}
jobs:
pack-module:
name: "Publish @deltachat/jsonrpc-client"
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
+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*
+202
View File
@@ -1,5 +1,201 @@
# Changelog
## [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
- Use `ubuntu-latest` runner for `@deltachat/jsonrpc-client` publishing.
## [1.159.2] - 2025-04-23
### Fixes
- Allow to send to chats after failed securejoin again ([#6817](https://github.com/chatmail/core/pull/6817)).
- Parse login scheme in `add_transport_from_qr()` ([#6802](https://github.com/chatmail/core/pull/6802)).
- Lowercase address in add_transport() ([#6805](https://github.com/chatmail/core/pull/6805)).
### API-Changes
- Rename add_transport() -> add_or_update_transport() ([#6800](https://github.com/chatmail/core/pull/6800)).
### Miscellaneous Tasks
- Update yerpc to 0.6.4.
- Clean up `deltachat-jsonrpc` dependencies.
### Refactor
- Move logins into SQL table ([#6724](https://github.com/chatmail/core/pull/6724)).
### Tests
- Check headers absense straightforwardly.
- Fix mismatch between the contact and the account in securejoin tests.
- Test that key of the recipient is gossiped in 1:1 chats.
## [1.159.1] - 2025-04-12
### API-Changes
- deltachat-rpc-client: Add `Account.add_transport()`.
- Add jsonrpc for info_contact_id.
### Build system
- Update crossbeam-channel from 0.5.14 to 0.5.15.
- Increase MSRV to 1.82.0.
### CI
- Don't make ruff format quiet ([#6785](https://github.com/chatmail/core/pull/6785)).
### Documentation
- MimeFactory.member_timestamps has the same order as To: rather than RCPT TO:.
- Two JsonRPC doc improvements ([#6778](https://github.com/chatmail/core/pull/6778)).
### Features / Changes
- Improve error message when the user tries to do AEAP ([#6786](https://github.com/chatmail/core/pull/6786)).
- Pass email and password via env in python-jsonrpc.
- Track gossiping per (chat, fingerprint) pair.
### Fixes
- Add missing ChatDeleted event to python jsonrpc client.
- Never send Autocrypt-Gossip in broadcast lists.
- Restart I/O when mvbox_move setting is changed.
### Tests
- Port test_delete_deltachat_folder to JSON-RPC.
- Autocrypt-Gossip header isn't sent in broadcast messages.
- Encrypt test_subject_in_group().
- Encrypt test_remove_member_bcc.
## [1.159.0] - 2025-04-08
### API-Changes
- deltachat-rpc-client: Add Message.get_info().
- CFFI: Add `dc_make_vcard()` and `dc_import_vcard()`.
- Add legacy Python bindings for `make_vcard` and `import_vcard`.
### CI
- Upgrade Rust from 1.84.1 to 1.86.0 ([#6784](https://github.com/chatmail/core/pull/6784)).
### Features / Changes
- Add name resp. "Me" to contact encryption info ([#6720](https://github.com/chatmail/core/pull/6720)).
- Get contact-id for info messages ([#6714](https://github.com/chatmail/core/pull/6714)).
- No unencrypted chat when securejoin times out ([#6722](https://github.com/chatmail/core/pull/6722)).
- Clear `Param::IsEdited` when forwarding a message.
- Remove email address from 'add second device' qr code ([#6760](https://github.com/chatmail/core/pull/6760)).
- Parse Proton Mail vCards again ([#6771](https://github.com/chatmail/core/pull/6771)).
- Do not consider encrypting to the primary OpenPGP key.
### Fixes
- jsonrpc: Fix deadlock in get_all_accounts().
- Set GroupNameTimestamp on group promotion ([#6729](https://github.com/chatmail/core/pull/6729)).
- Encrypt broadcast lists.
### Miscellaneous Tasks
- Update yerpc to 0.6.3.
- cargo: Update textwrap from 0.16.1 to 0.16.2.
- cargo: Bump uuid from 1.15.1 to 1.16.0.
- cargo: Bump libc from 0.2.170 to 0.2.171.
- cargo: Bump anyhow from 1.0.96 to 1.0.97.
- cargo: Bump bytes from 1.10.0 to 1.10.1.
- cargo: Bump once_cell from 1.20.3 to 1.21.3.
- cargo: Bump thiserror from 2.0.11 to 2.0.12.
- cargo: Bump pin-project from 1.1.9 to 1.1.10.
- cargo: Bump hyper-util from 0.1.10 to 0.1.11.
- cargo: Bump log from 0.4.26 to 0.4.27.
- cargo: Bump tokio-util from 0.7.13 to 0.7.14.
- cargo: Bump syn from 2.0.98 to 2.0.100.
- cargo: Bump serde_json from 1.0.139 to 1.0.140.
- cargo: Bump quote from 1.0.38 to 1.0.40.
- cargo: Bump http-body-util from 0.1.2 to 0.1.3.
- cargo: Bump openssl from 0.10.71 to 0.10.72.
- cargo: Bump quick-xml from 0.37.2 to 0.37.4.
- cargo: Bump blake3 from 1.6.1 to 1.8.0.
- cargo: Bump tokio from 1.43.0 to 1.43.1 ([#6780](https://github.com/chatmail/core/pull/6780)).
- Add issue template.
- Add bug label on bug issue template.
- cargo: Bump tokio from 1.43.0 to 1.44.1.
- cargo: Bump fd-lock from 4.0.2 to 4.0.4.
- Update async-smtp from 0.10.0 to 0.10.1.
- Update async-imap from 0.10.3 to 0.10.4.
- cargo: Bump tempfile from 3.14.0 to 3.19.1.
- cargo: Bump image from 0.25.5 to 0.25.6.
- cargo: Bump serde from 1.0.218 to 1.0.219.
### Other
- Add python and tox to flake.nix devshell ([#6233](https://github.com/chatmail/core/pull/6233))
- Update spec wrt edit/delete, minor rewordings ([#6708](https://github.com/chatmail/core/pull/6708))
- Update 'takes longer' fallback wording.
- Handle classic emails as such only in classic profiles ([#6767](https://github.com/chatmail/core/pull/6767))
- Move ASM strings to core, point to "Add Second Device" ([#6777](https://github.com/chatmail/core/pull/6777))
### Refactor
- Replace `once_cell::sync::Lazy` with `std::sync::LazyLock`.
- Move vCard code to its own file ([#6776](https://github.com/chatmail/core/pull/6776)).
### Tests
- Use encryption in more Rust tests.
- Use encryption in all JSON-RPC online tests.
- Encrypt legacy Python tests.
- Send only encrypted messages in online JS tests.
- Add APIs to create `dom@example.net` and `elena@example.net`.
- Split public keys from secret keys in runtime.
- Remove fetch_existing tests.
- Port test_forward_encrypted_to_unencrypted from legacy Python to Rust.
- Port test_one_account_send_bcc_setting from legacy Python to JSON-RPC.
- Port test_multidevice_sync_seen to JSON-RPC.
- Use QR codes to setup contact with test bots.
- Remove flaky key::tests::test_load_self_existing test ([#6763](https://github.com/chatmail/core/pull/6763)).
- Update blob hash in blob::blob_tests::test_selfavatar_outside_blobdir.
## [1.158.0] - 2025-03-29
### API-Changes
@@ -6101,3 +6297,9 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[1.157.2]: https://github.com/chatmail/core/compare/v1.157.1..v1.157.2
[1.157.3]: https://github.com/chatmail/core/compare/v1.157.2..v1.157.3
[1.158.0]: https://github.com/chatmail/core/compare/v1.157.3..v1.158.0
[1.159.0]: https://github.com/chatmail/core/compare/v1.158.0..v1.159.0
[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
Generated
+747 -757
View File
File diff suppressed because it is too large Load Diff
+27 -23
View File
@@ -1,9 +1,9 @@
[package]
name = "deltachat"
version = "1.158.0"
version = "1.159.5"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.81"
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,38 +46,38 @@ 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 = "7", default-features=false, features = ["std"] }
brotli = { version = "8", default-features=false, features = ["std"] }
bytes = "1"
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
data-encoding = "2.7.0"
data-encoding = "2.9.0"
escaper = "0.1"
fast-socks5 = "0.10"
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"
image = { version = "0.25.5", 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 }
hyper-util = "0.1.13"
image = { version = "0.25.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh-gossip = { version = "0.35", default-features = false, features = ["net"] }
iroh = { version = "0.35", default-features = false }
kamadak-exif = "0.6.1"
libc = { workspace = true }
mail-builder = { version = "0.4.2", default-features = false }
mail-builder = { version = "0.4.3", default-features = false }
mailparse = { workspace = true }
mime = "0.3.17"
num_cpus = "1.16"
num_cpus = "1.17"
num-derive = "0.4"
num-traits = { workspace = true }
parking_lot = "0.12"
parking_lot = "0.12.4"
percent-encoding = "2.3"
pgp = { version = "0.15.0", default-features = false }
pgp = { version = "0.16.0", default-features = false }
pin-project = "1"
qrcodegen = "1.7.0"
quick-xml = "0.37"
@@ -83,7 +86,7 @@ rand = { workspace = true }
regex = { workspace = true }
rusqlite = { workspace = true, features = ["sqlcipher"] }
rust-hsluv = "0.1"
rustls-pki-types = "1.11.0"
rustls-pki-types = "1.12.0"
rustls = { version = "0.23.22", default-features = false }
sanitize-filename = { workspace = true }
serde_json = { workspace = true }
@@ -91,8 +94,8 @@ 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"] }
smallvec = "1.14.0"
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"
tagger = "4.3.4"
@@ -108,11 +111,11 @@ toml = "0.8"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
webpki-roots = "0.26.8"
blake3 = "1.8.0"
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 }
@@ -173,7 +176,7 @@ harness = false
anyhow = "1"
async-channel = "2.3.1"
base64 = "0.22"
chrono = { version = "0.4.40", default-features = false }
chrono = { version = "0.4.41", default-features = false }
deltachat-contact-tools = { path = "deltachat-contact-tools" }
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
deltachat = { path = ".", default-features = false }
@@ -190,18 +193,19 @@ rusqlite = "0.32"
sanitize-filename = "0.5"
serde = "1.0"
serde_json = "1"
tempfile = "3.14.0"
tempfile = "3.20.0"
thiserror = "2"
tokio = "1"
tokio-util = "0.7.14"
tracing-subscriber = "0.3"
yerpc = "0.6.2"
yerpc = "0.6.4"
[features]
default = ["vendored"]
internals = []
vendored = [
"rusqlite/bundled-sqlcipher-vendored-openssl"
"rusqlite/bundled-sqlcipher-vendored-openssl",
"async-native-tls/vendored"
]
[lints.rust]
+10 -22
View File
@@ -49,12 +49,12 @@ $ curl https://sh.rustup.rs -sSf | sh
## Using the CLI client
Compile and run Delta Chat Core command line utility, using `cargo`:
Compile and run the command line utility, using `cargo`:
```
$ cargo run --locked -p deltachat-repl -- ~/deltachat-db
$ cargo run --locked -p deltachat-repl -- ~/profile-db
```
where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist.
where ~/profile-db is the database file. The utility will create it if it does not exist.
Optionally, install `deltachat-repl` binary with
```
@@ -62,13 +62,13 @@ $ cargo install --locked --path deltachat-repl/
```
and run as
```
$ deltachat-repl ~/deltachat-db
$ deltachat-repl ~/profile-db
```
Configure your account (if not already configured):
```
Delta Chat Core is awaiting your commands.
Chatmail is awaiting your commands.
> set addr your@email.org
> set mail_pw yourpassword
> configure
@@ -106,11 +106,6 @@ Single#10: yourfriends@email.org [yourfriends@email.org]
Message sent.
```
If `yourfriend@email.org` uses DeltaChat, but does not receive message just
sent, it is advisable to check `Spam` folder. It is known that at least
`gmx.com` treat such test messages as spam, unless told otherwise with web
interface.
List messages when inside a chat:
```
@@ -161,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`,
@@ -175,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.
@@ -187,11 +177,9 @@ $ cargo bolero test fuzz_format_flowed --release=false -e afl -s NONE
## Update Provider Data
To add the updates from the
[provider-db](https://github.com/deltachat/provider-db) to the core, run:
```
./src/provider/update.py ../provider-db/_providers/ > src/provider/data.rs
```
[provider-db](https://github.com/chatmail/provider-db) to the core,
check line `REV=` inside `./scripts/update-provider-database.sh`
and then run the script.
## Language bindings and frontend projects
+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;
+9
View File
@@ -20,6 +20,8 @@ pub struct VcardContact {
pub key: Option<String>,
/// The contact's profile image (=avatar) in Base64, vcard property `photo`
pub profile_image: Option<String>,
/// The biography, stored in the vcard property `note`
pub biography: Option<String>,
/// The timestamp when the vcard was created / last updated, vcard property `rev`
pub timestamp: Result<i64>,
}
@@ -60,6 +62,9 @@ pub fn make_vcard(contacts: &[VcardContact]) -> String {
if let Some(profile_image) = &c.profile_image {
res += &format!("PHOTO:data:image/jpeg;base64,{profile_image}\r\n");
}
if let Some(biography) = &c.biography {
res += &format!("NOTE:{biography}\r\n");
}
if let Some(timestamp) = format_timestamp(c) {
res += &format!("REV:{timestamp}\r\n");
}
@@ -186,6 +191,7 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
let mut addr = None;
let mut key = None;
let mut photo = None;
let mut biography = None;
let mut datetime = None;
for mut line in lines.by_ref() {
@@ -205,6 +211,8 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
key.get_or_insert(k);
} else if let Some(p) = base64_photo(line) {
photo.get_or_insert(p);
} else if let Some((_params, bio)) = vcard_property(line, "note") {
biography.get_or_insert(bio);
} else if let Some((_params, rev)) = vcard_property(line, "rev") {
datetime.get_or_insert(rev);
} else if line.eq_ignore_ascii_case("END:VCARD") {
@@ -216,6 +224,7 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
addr,
key: key.map(|s| s.to_string()),
profile_image: photo.map(|s| s.to_string()),
biography: biography.map(|b| b.to_owned()),
timestamp: datetime
.context("No timestamp in vcard")
.and_then(parse_datetime),
@@ -91,6 +91,7 @@ fn test_make_and_parse_vcard() {
authname: "Alice Wonderland".to_string(),
key: Some("[base64-data]".to_string()),
profile_image: Some("image in Base64".to_string()),
biography: Some("Hi, I'm Alice".to_string()),
timestamp: Ok(1713465762),
},
VcardContact {
@@ -98,6 +99,7 @@ fn test_make_and_parse_vcard() {
authname: "".to_string(),
key: None,
profile_image: None,
biography: None,
timestamp: Ok(0),
},
];
@@ -108,6 +110,7 @@ fn test_make_and_parse_vcard() {
FN:Alice Wonderland\r\n\
KEY:data:application/pgp-keys;base64,[base64-data]\r\n\
PHOTO:data:image/jpeg;base64,image in Base64\r\n\
NOTE:Hi, I'm Alice\r\n\
REV:20240418T184242Z\r\n\
END:VCARD\r\n",
"BEGIN:VCARD\r\n\
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.158.0"
version = "1.159.5"
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>
+5 -4
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
@@ -7595,7 +7596,7 @@ void dc_event_unref(dc_event_t* event);
/// @deprecated 2025-03
#define DC_STR_SECUREJOIN_WAIT_TIMEOUT 191
/// "That seems to take longer, maybe the contact or you are offline. However, the process continues in background, you can do something else…"
/// "The contact must be online to proceed. This process will continue automatically in background."
///
/// Used as info message.
#define DC_STR_SECUREJOIN_TAKES_LONGER 192
+6 -5
View File
@@ -37,6 +37,7 @@ use deltachat::*;
use deltachat::{accounts::Accounts, log::LogExt};
use deltachat_jsonrpc::api::CommandApi;
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
use message::Viewtype;
use num_traits::{FromPrimitive, ToPrimitive};
use rand::Rng;
use tokio::runtime::Runtime;
@@ -536,7 +537,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::IncomingReaction { .. } => 2002,
EventType::IncomingWebxdcNotify { .. } => 2003,
EventType::IncomingMsg { .. } => 2005,
EventType::IncomingMsgBunch { .. } => 2006,
EventType::IncomingMsgBunch => 2006,
EventType::MsgsNoticed { .. } => 2008,
EventType::MsgDelivered { .. } => 2010,
EventType::MsgFailed { .. } => 2012,
@@ -594,7 +595,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::ConnectivityChanged
| EventType::SelfavatarChanged
| EventType::ConfigSynced { .. }
| EventType::IncomingMsgBunch { .. }
| EventType::IncomingMsgBunch
| EventType::ErrorSelfNotInGroup(_)
| EventType::AccountsBackgroundFetchDone
| EventType::ChatlistChanged
@@ -669,7 +670,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::MsgsNoticed(_)
| EventType::ConnectivityChanged
| EventType::WebxdcInstanceDeleted { .. }
| EventType::IncomingMsgBunch { .. }
| EventType::IncomingMsgBunch
| EventType::SelfavatarChanged
| EventType::AccountsBackgroundFetchDone
| EventType::ChatlistChanged
@@ -771,7 +772,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
| EventType::AccountsBackgroundFetchDone
| EventType::ChatEphemeralTimerModified { .. }
| EventType::ChatDeleted { .. }
| EventType::IncomingMsgBunch { .. }
| EventType::IncomingMsgBunch
| EventType::ChatlistItemChanged { .. }
| EventType::ChatlistChanged
| EventType::AccountsChanged
@@ -2076,7 +2077,7 @@ pub unsafe extern "C" fn dc_get_msg(context: *mut dc_context_t, msg_id: u32) ->
ctx,
"dc_get_msg called with special msg_id={msg_id}, returning empty msg"
);
message::Message::default()
message::Message::new(Viewtype::default())
} else {
warn!(ctx, "dc_get_msg could not retrieve msg_id {msg_id}: {e:#}");
return ptr::null_mut();
+3 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.158.0"
version = "1.159.5"
description = "DeltaChat JSON-RPC API"
edition = "2021"
license = "MPL-2.0"
@@ -13,10 +13,7 @@ deltachat-contact-tools = { workspace = true }
num-traits = { workspace = true }
schemars = "0.8.22"
serde = { workspace = true, features = ["derive"] }
tempfile = { workspace = true }
log = { workspace = true }
async-channel = { workspace = true }
futures = { workspace = true }
serde_json = { workspace = true }
yerpc = { workspace = true, features = ["anyhow_expose", "openrpc"] }
typescript-type-def = { version = "0.5.13", features = ["json_value"] }
@@ -27,6 +24,8 @@ base64 = { workspace = true }
[dev-dependencies]
tokio = { workspace = true, features = ["full", "rt-multi-thread"] }
tempfile = { workspace = true }
futures = { workspace = true }
[features]
+51 -7
View File
@@ -327,8 +327,12 @@ impl CommandApi {
.get_config_bool(deltachat::config::Config::ProxyEnabled)
.await?;
let provider_info =
get_provider_info(&ctx, email.split('@').last().unwrap_or(""), proxy_enabled).await;
let provider_info = get_provider_info(
&ctx,
email.split('@').next_back().unwrap_or(""),
proxy_enabled,
)
.await;
Ok(ProviderInfo::from_dc_type(provider_info))
}
@@ -435,7 +439,7 @@ impl CommandApi {
/// Setup the credential config before calling this.
///
/// Deprecated as of 2025-02; use `add_transport_from_qr()`
/// or `add_transport()` instead.
/// or `add_or_update_transport()` instead.
async fn configure(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.stop_io().await;
@@ -479,21 +483,30 @@ impl CommandApi {
/// from a server encoded in a QR code.
/// - [Self::list_transports()] to get a list of all configured transports.
/// - [Self::delete_transport()] to remove a transport.
async fn add_transport(&self, account_id: u32, param: EnteredLoginParam) -> Result<()> {
async fn add_or_update_transport(
&self,
account_id: u32,
param: EnteredLoginParam,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.add_transport(&param.try_into()?).await
ctx.add_or_update_transport(&mut param.try_into()?).await
}
/// Deprecated 2025-04. Alias for [Self::add_or_update_transport()].
async fn add_transport(&self, account_id: u32, param: EnteredLoginParam) -> Result<()> {
self.add_or_update_transport(account_id, param).await
}
/// Adds a new email account as a transport
/// using the server encoded in the QR code.
/// See [Self::add_transport].
/// See [Self::add_or_update_transport].
async fn add_transport_from_qr(&self, account_id: u32, qr: String) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.add_transport_from_qr(&qr).await
}
/// Returns the list of all email accounts that are used as a transport in the current profile.
/// Use [Self::add_transport()] to add or change a transport
/// Use [Self::add_or_update_transport()] to add or change a transport
/// and [Self::delete_transport()] to delete a transport.
async fn list_transports(&self, account_id: u32) -> Result<Vec<EnteredLoginParam>> {
let ctx = self.get_context(account_id).await?;
@@ -2272,6 +2285,37 @@ impl CommandApi {
// mimics the old desktop call, will get replaced with something better in the composer rewrite,
// the better version will just be sending the current draft, though there will be probably something similar with more options to this for the corner cases like setting a marker on the map
/// Send a message to a chat.
///
/// This function returns after the message has been placed in the sending queue.
/// This does not imply that the message was really sent out yet.
/// However, from your view, you're done with the message.
/// Sooner or later it will find its way.
///
/// **Attaching files:**
///
/// Pass the file path in the `file` parameter.
/// If `file` is not in the blob directory yet,
/// it will be copied into the blob directory.
/// If you want, you can delete the file immediately after this function returns.
///
/// You can also write the attachment directly into the blob directory
/// and then pass the path as the `file` parameter;
/// this will prevent an unnecessary copying of the file.
///
/// In `filename`, you can pass the original name of the file,
/// which will then be shown in the UI.
/// in this case the current name of `file` on the filesystem will be ignored.
///
/// In order to deduplicate files that contain the same data,
/// the file will be named `<hash>.<extension>`, e.g. `ce940175885d7b78f7b7e9f1396611f.jpg`.
///
/// NOTE:
/// - This function will rename the file. To get the new file path, call `get_file()`.
/// - The file must not be modified after this function was called.
/// - Images etc. will NOT be recoded.
/// In order to recode images,
/// use `misc_set_draft` and pass `Image` as the viewtype.
#[expect(clippy::too_many_arguments)]
async fn misc_send_msg(
&self,
@@ -5,7 +5,9 @@ use serde::Serialize;
use yerpc::TypeDef;
/// Login parameters entered by the user.
///
/// Usually it will be enough to only set `addr` and `password`,
/// and all the other settings will be autoconfigured.
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct EnteredLoginParam {
+13 -1
View File
@@ -8,6 +8,7 @@ use deltachat::chat::ChatVisibility;
use deltachat::contact::Contact;
use deltachat::context::Context;
use deltachat::download;
use deltachat::log::LogExt as _;
use deltachat::message::Message;
use deltachat::message::MsgId;
use deltachat::message::Viewtype;
@@ -23,6 +24,7 @@ use super::webxdc::WebxdcMessageInfo;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase", tag = "kind")]
#[expect(clippy::large_enum_variant)]
pub enum MessageLoadResult {
Message(MessageObject),
LoadingError { error: String },
@@ -70,6 +72,9 @@ pub struct MessageObject {
/// when is_info is true this describes what type of system message it is
system_message_type: SystemMessageType,
/// if is_info is set, this refers to the contact profile that should be opened when the info message is tapped.
info_contact_id: Option<u32>,
duration: i32,
dimensions_height: i32,
dimensions_width: i32,
@@ -140,7 +145,10 @@ impl MessageObject {
let override_sender_name = message.get_override_sender_name();
let webxdc_info = if message.get_viewtype() == Viewtype::Webxdc {
Some(WebxdcMessageInfo::get_for_message(context, msg_id).await?)
WebxdcMessageInfo::get_for_message(context, msg_id)
.await
.log_err(context)
.ok()
} else {
None
};
@@ -228,6 +236,10 @@ impl MessageObject {
is_forwarded: message.is_forwarded(),
is_bot: message.is_bot(),
system_message_type: message.get_info_type().into(),
info_contact_id: message
.get_info_contact_id(context)
.await?
.map(|id| id.to_u32()),
duration: message.get_duration(),
dimensions_height: message.get_height(),
+14 -14
View File
@@ -2,31 +2,31 @@
"author": "Delta Chat Developers (ML) <delta@codespeak.net>",
"dependencies": {
"@deltachat/tiny-emitter": "3.0.0",
"isomorphic-ws": "^4.0.1",
"isomorphic-ws": "^5.0.0",
"yerpc": "^0.6.2"
},
"devDependencies": {
"@types/chai": "^4.2.21",
"@types/chai-as-promised": "^7.1.5",
"@types/mocha": "^9.0.0",
"@types/ws": "^7.2.4",
"c8": "^7.10.0",
"@types/chai": "^4.3.10",
"@types/chai-as-promised": "^7.1.8",
"@types/mocha": "^10.0.4",
"@types/ws": "^8.5.9",
"c8": "^8.0.1",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"esbuild": "^0.17.9",
"esbuild": "^0.25.5",
"http-server": "^14.1.1",
"mocha": "^9.1.1",
"mocha": "^10.2.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.6.2",
"typedoc": "^0.23.2",
"typescript": "^4.5.5",
"prettier": "^3.5.3",
"typedoc": "^0.28.5",
"typescript": "^5.8.3",
"ws": "^8.5.0"
},
"exports": {
".": {
"types": "./dist/deltachat.d.ts",
"import": "./dist/deltachat.js",
"require": "./dist/deltachat.cjs",
"types": "./dist/deltachat.d.ts"
"require": "./dist/deltachat.cjs"
}
},
"license": "MPL-2.0",
@@ -54,5 +54,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.158.0"
"version": "1.159.5"
}
@@ -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.158.0"
version = "1.159.5"
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"] }
+2 -11
View File
@@ -1162,17 +1162,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let reaction = arg2;
send_reaction(&context, msg_id, reaction).await?;
}
"listcontacts" | "contacts" | "listverified" => {
let contacts = Contact::get_all(
&context,
if arg0 == "listverified" {
DC_GCL_VERIFIED_ONLY | DC_GCL_ADD_SELF
} else {
DC_GCL_ADD_SELF
},
Some(arg1),
)
.await?;
"listcontacts" | "contacts" => {
let contacts = Contact::get_all(&context, DC_GCL_ADD_SELF, Some(arg1)).await?;
log_contactlist(&context, &contacts).await?;
println!("{} contacts.", contacts.len());
}
+1 -1
View File
@@ -323,7 +323,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
}
});
println!("Delta Chat Core is awaiting your commands.");
println!("Chatmail is awaiting your commands.");
let config = Config::builder()
.history_ignore_space(true)
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "1.158.0"
version = "1.159.5"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -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
@@ -1,4 +1,5 @@
import argparse
import os
import re
import sys
from threading import Thread
@@ -89,8 +90,8 @@ def _run_cli(
help="accounts folder (default: current working directory)",
nargs="?",
)
parser.add_argument("--email", action="store", help="email address")
parser.add_argument("--password", action="store", help="password")
parser.add_argument("--email", action="store", help="email address", default=os.getenv("DELTACHAT_EMAIL"))
parser.add_argument("--password", action="store", help="password", default=os.getenv("DELTACHAT_PASSWORD"))
args = parser.parse_args(argv[1:])
with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
@@ -114,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)
@@ -123,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:
@@ -142,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
@@ -110,6 +119,17 @@ class Account:
"""Configure an account."""
yield self._rpc.configure.future(self.id)
@futuremethod
def add_or_update_transport(self, params):
"""Add a new transport."""
yield self._rpc.add_or_update_transport.future(self.id, params)
@futuremethod
def list_transports(self):
"""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
def bring_online(self):
"""Start I/O and wait until IMAP becomes IDLE."""
self.start_io()
@@ -147,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]
@@ -190,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.
@@ -199,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
@@ -216,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(
@@ -279,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.
@@ -350,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"
@@ -48,6 +55,7 @@ class EventType(str, Enum):
MSG_READ = "MsgRead"
MSG_DELETED = "MsgDeleted"
CHAT_MODIFIED = "ChatModified"
CHAT_DELETED = "ChatDeleted"
CHAT_EPHEMERAL_TIMER_MODIFIED = "ChatEphemeralTimerModified"
CONTACTS_CHANGED = "ContactsChanged"
LOCATION_CHANGED = "LocationChanged"
@@ -70,7 +78,7 @@ class EventType(str, Enum):
class ChatId(IntEnum):
"""Special chat ids"""
"""Special chat IDs."""
TRASH = 3
ARCHIVED_LINK = 6
@@ -79,7 +87,7 @@ class ChatId(IntEnum):
class ChatType(IntEnum):
"""Chat types"""
"""Chat type."""
UNDEFINED = 0
SINGLE = 100
@@ -89,7 +97,7 @@ class ChatType(IntEnum):
class ChatVisibility(str, Enum):
"""Chat visibility types"""
"""Chat visibility types."""
NORMAL = "Normal"
ARCHIVED = "Archived"
@@ -97,7 +105,7 @@ class ChatVisibility(str, Enum):
class DownloadState(str, Enum):
"""Message download state"""
"""Message download state."""
DONE = "Done"
AVAILABLE = "Available"
@@ -158,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
@@ -173,7 +181,7 @@ class CertificateChecks(IntEnum):
class Connectivity(IntEnum):
"""Connectivity states"""
"""Connectivity states."""
NOT_CONNECTED = 1000
CONNECTING = 2000
@@ -182,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
@@ -192,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
@@ -214,7 +222,7 @@ class ProviderStatus(IntEnum):
class PushNotifyState(IntEnum):
"""Push notifications state"""
"""Push notifications state."""
NOT_CONNECTED = 0
HEARTBEAT = 1
@@ -222,7 +230,7 @@ class PushNotifyState(IntEnum):
class ShowEmails(IntEnum):
"""Show emails mode"""
"""Show emails mode."""
OFF = 0
ACCEPTED_CONTACTS = 1
@@ -230,7 +238,7 @@ class ShowEmails(IntEnum):
class SocketSecurity(IntEnum):
"""Socket security"""
"""Socket security."""
AUTOMATIC = 0
SSL = 1
@@ -239,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,33 +15,40 @@ 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}
yield account._rpc.add_transport.future(account.id, params)
yield account.add_or_update_transport.future(params)
assert account.is_configured()
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:
@@ -17,7 +17,7 @@ def test_event_on_configuration(acfactory: ACFactory) -> None:
account = acfactory.get_unconfigured_account()
account.clear_all_events()
assert not account.is_configured()
future = account._rpc.add_transport.future(account.id, {"addr": addr, "password": password})
future = account.add_or_update_transport.future({"addr": addr, "password": password})
while True:
event = account.wait_for_event()
if event.kind == EventType.ACCOUNTS_ITEM_CHANGED:
+57 -15
View File
@@ -63,8 +63,7 @@ def test_acfactory(acfactory) -> None:
def test_configure_starttls(acfactory) -> None:
addr, password = acfactory.get_credentials()
account = acfactory.get_unconfigured_account()
account._rpc.add_transport(
account.id,
account.add_or_update_transport(
{
"addr": addr,
"password": password,
@@ -75,14 +74,36 @@ def test_configure_starttls(acfactory) -> None:
assert account.is_configured()
def test_lowercase_address(acfactory) -> None:
addr, password = acfactory.get_credentials()
addr_upper = addr.upper()
account = acfactory.get_unconfigured_account()
account.add_or_update_transport(
{
"addr": addr_upper,
"password": password,
},
)
assert account.is_configured()
assert addr_upper != addr
assert account.get_config("configured_addr") == addr
assert account.list_transports()[0]["addr"] == addr
for param in [
account.get_info()["used_account_settings"],
account.get_info()["entered_account_settings"],
]:
assert addr in param
assert addr_upper not in param
def test_configure_ip(acfactory) -> None:
addr, password = acfactory.get_credentials()
account = acfactory.get_unconfigured_account()
ip_address = socket.gethostbyname(addr.rsplit("@")[-1])
with pytest.raises(JsonRpcError):
account._rpc.add_transport(
account.id,
account.add_or_update_transport(
{
"addr": addr,
"password": password,
@@ -96,8 +117,7 @@ def test_configure_alternative_port(acfactory) -> None:
"""Test that configuration with alternative port 443 works."""
addr, password = acfactory.get_credentials()
account = acfactory.get_unconfigured_account()
account._rpc.add_transport(
account.id,
account.add_or_update_transport(
{
"addr": addr,
"password": password,
@@ -111,15 +131,14 @@ def test_configure_alternative_port(acfactory) -> None:
def test_list_transports(acfactory) -> None:
addr, password = acfactory.get_credentials()
account = acfactory.get_unconfigured_account()
account._rpc.add_transport(
account.id,
account.add_or_update_transport(
{
"addr": addr,
"password": password,
"imapUser": addr,
},
)
transports = account._rpc.list_transports(account.id)
transports = account.list_transports()
assert len(transports) == 1
params = transports[0]
assert params["addr"] == addr
@@ -424,7 +443,7 @@ def test_wait_next_messages(acfactory) -> None:
addr, password = acfactory.get_credentials()
bot = acfactory.get_unconfigured_account()
bot.set_config("bot", "1")
bot._rpc.add_transport(bot.id, {"addr": addr, "password": password})
bot.add_or_update_transport({"addr": addr, "password": password})
assert bot.is_configured()
# There are no old messages and the call returns immediately.
@@ -607,7 +626,7 @@ def test_reactions_for_a_reordering_move(acfactory, direct_imap):
addr, password = acfactory.get_credentials()
ac2 = acfactory.get_unconfigured_account()
ac2._rpc.add_transport(ac2.id, {"addr": addr, "password": password})
ac2.add_or_update_transport({"addr": addr, "password": password})
ac2.set_config("mvbox_move", "1")
assert ac2.is_configured()
@@ -717,12 +736,11 @@ def test_get_http_response(acfactory):
def test_configured_imap_certificate_checks(acfactory):
alice = acfactory.new_configured_account()
configured_certificate_checks = alice.get_config("configured_imap_certificate_checks")
# Certificate checks should be configured (not None)
assert configured_certificate_checks
assert "cert_automatic" in alice.get_info().used_account_settings
# 0 is the value old Delta Chat core versions used
# "cert_old_automatic" is the value old Delta Chat core versions used
# to mean user entered "imap_certificate_checks=0" (Automatic)
# and configuration failed to use strict TLS checks
# so it switched strict TLS checks off.
@@ -733,7 +751,7 @@ def test_configured_imap_certificate_checks(acfactory):
#
# Core 1.142.4, 1.142.5 and 1.142.6 saved this value due to bug.
# This test is a regression test to prevent this happening again.
assert configured_certificate_checks != "0"
assert "cert_old_automatic" not in alice.get_info().used_account_settings
def test_no_old_msg_is_fresh(acfactory):
@@ -805,3 +823,27 @@ def test_get_all_accounts_deadlock(rpc):
all_accounts = rpc.get_all_accounts.future()
rpc.add_account()
all_accounts()
def test_delete_deltachat_folder(acfactory, direct_imap):
"""Test that DeltaChat folder is recreated if user deletes it manually."""
ac1 = acfactory.new_configured_account()
ac1.set_config("mvbox_move", "1")
ac1.bring_online()
ac1_direct_imap = direct_imap(ac1)
ac1_direct_imap.conn.folder.delete("DeltaChat")
assert "DeltaChat" not in ac1_direct_imap.list_folders()
# Wait until new folder is created and UIDVALIDITY is updated.
while True:
event = ac1.wait_for_event()
if event.kind == EventType.INFO and "uid/validity change folder DeltaChat" in event.msg:
break
ac2 = acfactory.get_online_account()
ac2.create_chat(ac1).send_text("hello")
msg = ac1.wait_for_incoming_msg().get_snapshot()
assert msg.text == "hello"
assert "DeltaChat" in ac1_direct_imap.list_folders()
+1 -1
View File
@@ -21,7 +21,7 @@ skip_install = True
deps =
ruff
commands =
ruff format --quiet --diff src/ examples/ tests/
ruff format --diff src/ examples/ tests/
ruff check src/ examples/ tests/
[pytest]
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.158.0"
version = "1.159.5"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"
@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "1.158.0"
"version": "1.159.5"
}
+8 -8
View File
@@ -10,9 +10,6 @@ ignore = [
# Unmaintained instant
"RUSTSEC-2024-0384",
# Unmaintained backoff
"RUSTSEC-2025-0012",
# Unmaintained paste
"RUSTSEC-2024-0436",
]
@@ -25,7 +22,8 @@ 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" },
@@ -34,20 +32,21 @@ skip = [
{ name = "linux-raw-sys", version = "0.4.14" },
{ name = "loom", version = "0.5.6" },
{ 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" },
{ name = "lru", version = "0.12.3" },
{ name = "thiserror-impl", version = "1.0.69" },
{ name = "thiserror", version = "1.0.69" },
{ name = "wasi", version = "0.11.0+wasi-snapshot-preview1" },
@@ -84,6 +83,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 }
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "1.158.0"
version = "1.159.5"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"
requires-python = ">=3.8"
-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)
+1 -5
View File
@@ -482,12 +482,8 @@ class ACFactory:
addr = f"{acname}@offline.org"
ac.update_config(
{
"addr": addr,
"displayname": acname,
"mail_pw": "123",
"configured_addr": addr,
"configured_mail_pw": "123",
"configured": "1",
"displayname": acname,
},
)
self._preconfigure_key(ac)
+6 -12
View File
@@ -16,8 +16,6 @@ class TestGroupStressTests:
lp.sec("ac1: send message to new group chat")
msg1 = chat.send_text("hello")
assert msg1.is_encrypted()
gossiped_timestamp = chat.get_summary()["gossiped_timestamp"]
assert gossiped_timestamp > 0
assert chat.num_contacts() == 3 + 1
@@ -46,19 +44,13 @@ class TestGroupStressTests:
assert to_remove.addr in sysmsg.text
assert sysmsg.chat.num_contacts() == 3
# Receiving message about removed contact does not reset gossip
assert chat.get_summary()["gossiped_timestamp"] == gossiped_timestamp
lp.sec("ac1: sending another message to the chat")
chat.send_text("hello2")
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "hello2"
assert chat.get_summary()["gossiped_timestamp"] == gossiped_timestamp
lp.sec("ac1: adding fifth member to the chat")
chat.add_contact(ac5)
# Adding contact to chat resets gossiped_timestamp
assert chat.get_summary()["gossiped_timestamp"] >= gossiped_timestamp
lp.sec("ac2: receiving system message about contact addition")
sysmsg = ac2._evtracker.wait_next_incoming_message()
@@ -442,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()
@@ -482,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")
@@ -508,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 -69
View File
@@ -798,12 +798,6 @@ def test_send_and_receive_will_encrypt_decrypt(acfactory, lp):
msg3.mark_seen()
assert not list(ac1.get_fresh_messages())
# Test that we do not gossip peer keys in 1-to-1 chat,
# as it makes no sense to gossip to peers their own keys.
# Gossip is only sent in encrypted messages,
# and we sent encrypted msg_back right above.
assert chat2b.get_summary()["gossiped_timestamp"] == 0
lp.sec("create group chat with two members, one of which has no encrypt state")
chat = ac1.create_group_chat("encryption test")
chat.add_contact(ac2)
@@ -813,41 +807,6 @@ def test_send_and_receive_will_encrypt_decrypt(acfactory, lp):
ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
def test_gossip_optimization(acfactory, lp):
"""Test that gossip timestamp is updated when someone else sends gossip,
so we don't have to send gossip ourselves.
"""
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
acfactory.introduce_each_other([ac1, ac2])
acfactory.introduce_each_other([ac2, ac3])
lp.sec("ac1 creates a group chat with ac2")
group_chat = ac1.create_group_chat("hello")
group_chat.add_contact(ac2)
msg = group_chat.send_text("hi")
# No Autocrypt gossip was sent yet.
gossiped_timestamp = msg.chat.get_summary()["gossiped_timestamp"]
assert gossiped_timestamp == 0
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.is_encrypted()
assert msg.text == "hi"
lp.sec("ac2 adds ac3 to the group")
msg.chat.add_contact(ac3)
lp.sec("ac1 receives message from ac2 and updates gossip timestamp")
msg = ac1._evtracker.wait_next_incoming_message()
assert msg.is_encrypted()
# ac1 has updated the gossip timestamp even though no gossip was sent by ac1.
# ac1 does not need to send gossip because ac2 already did it.
gossiped_timestamp = msg.chat.get_summary()["gossiped_timestamp"]
assert gossiped_timestamp == int(msg.time_sent.timestamp())
def test_send_first_message_as_long_unicode_with_cr(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
@@ -1543,15 +1502,6 @@ def test_connectivity(acfactory, lp):
assert len(msgs) == 2
assert msgs[1].text == "Hi 2"
lp.sec("Test that the connectivity is NOT_CONNECTED if the password is wrong")
ac1.set_config("configured_mail_pw", "abc")
ac1.stop_io()
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
ac1.start_io()
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTING)
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
def test_fetch_deleted_msg(acfactory, lp):
"""This is a regression test: Messages with \\Deleted flag were downloaded again and again,
@@ -1667,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
@@ -1703,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
@@ -1979,23 +1929,6 @@ def test_scan_folders(acfactory, lp, folder, move, expected_destination):
assert len(ac1.direct_imap.get_all_messages()) == 0
def test_delete_deltachat_folder(acfactory):
"""Test that DeltaChat folder is recreated if user deletes it manually."""
ac1 = acfactory.new_online_configuring_account(mvbox_move=True)
ac2 = acfactory.new_online_configuring_account()
acfactory.wait_configured(ac1)
ac1.direct_imap.conn.folder.delete("DeltaChat")
assert "DeltaChat" not in ac1.direct_imap.list_folders()
acfactory.bring_accounts_online()
ac2.create_chat(ac1).send_text("hello")
msg = ac1._evtracker.wait_next_incoming_message()
assert msg.text == "hello"
assert "DeltaChat" in ac1.direct_imap.list_folders()
def test_archived_muted_chat(acfactory, lp):
"""If an archived and muted chat receives a new message, DC_EVENT_MSGS_CHANGED for
DC_CHAT_ID_ARCHIVED_LINK must be generated if the chat had only seen messages previously.
-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
@@ -45,7 +45,7 @@ deps =
pygments
restructuredtext_lint
commands =
ruff format --quiet --diff setup.py src/deltachat examples/ tests/
ruff format --diff setup.py src/deltachat examples/ tests/
ruff check src/deltachat tests/ examples/
rst-lint --encoding 'utf-8' README.rst
+1 -1
View File
@@ -1 +1 @@
2025-03-29
2025-05-14
+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.84.1
RUST_VERSION=1.87.0
ARCH="$(uname -m)"
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
+10 -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"))]
@@ -304,12 +302,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 +309,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].
+15 -2
View File
@@ -93,7 +93,7 @@ impl<'a> BlobObject<'a> {
if let Some(extension) = original_name.extension().filter(|e| e.len() <= 32) {
let extension = extension.to_string_lossy().to_lowercase();
let extension = sanitize_filename(&extension);
format!("$BLOBDIR/{hash}.{}", extension)
format!("$BLOBDIR/{hash}.{extension}")
} else {
format!("$BLOBDIR/{hash}")
};
@@ -432,7 +432,20 @@ impl<'a> BlobObject<'a> {
if mem::take(&mut add_white_bg) {
self::add_white_bg(&mut img);
}
let new_img = img.thumbnail(img_wh, img_wh);
// resize() results in often slightly better quality,
// however, comes at high price of being 4+ times slower than thumbnail().
// for a typical camera image that is sent, this may be a change from "instant" (500ms) to "long time waiting" (3s).
// as we do not have recoding in background while chat has already a preview,
// we vote for speed.
// exception is the avatar image: this is far more often sent than recoded,
// usually has less pixels by cropping, UI that needs to wait anyways,
// and also benefits from slightly better (5%) encoding of Triangle-filtered images.
let new_img = if is_avatar {
img.resize(img_wh, img_wh, image::imageops::FilterType::Triangle)
} else {
img.thumbnail(img_wh, img_wh)
};
if encoded_img_exceeds_bytes(
context,
+2 -2
View File
@@ -174,7 +174,7 @@ async fn test_selfavatar_outside_blobdir() {
let avatar_blob = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
let avatar_path = Path::new(&avatar_blob);
assert!(
avatar_blob.ends_with("009161310a6afc319163e4bcabd23b9.jpg"),
avatar_blob.ends_with("7dde69e06b5ae6c27520a436bbfd65b.jpg"),
"The avatar filename should be its hash, put instead it's {avatar_blob}"
);
let scaled_avatar_size = file_size(avatar_path).await;
@@ -226,7 +226,7 @@ async fn test_selfavatar_in_blobdir() {
.unwrap();
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
assert!(
avatar_cfg.ends_with("ec054c444a5755adf2b0aaea40209f2.png"),
avatar_cfg.ends_with("d57cb5ce5f371531b6e1fb17b6dd1af.png"),
"Avatar file name {avatar_cfg} should end with its hash"
);
+102 -101
View File
@@ -653,7 +653,7 @@ impl ChatId {
) -> Result<()> {
let chat_id = ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Yes)
.await
.with_context(|| format!("can't create chat for {}", contact_id))?;
.with_context(|| format!("can't create chat for {contact_id}"))?;
chat_id
.set_protection(
context,
@@ -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,
@@ -1376,41 +1378,10 @@ impl ChatId {
}
pub(crate) async fn reset_gossiped_timestamp(self, context: &Context) -> Result<()> {
self.set_gossiped_timestamp(context, 0).await
}
/// Get timestamp of the last gossip sent in the chat.
/// Zero return value means that gossip was never sent.
pub async fn get_gossiped_timestamp(self, context: &Context) -> Result<i64> {
let timestamp: Option<i64> = context
.sql
.query_get_value("SELECT gossiped_timestamp FROM chats WHERE id=?;", (self,))
.await?;
Ok(timestamp.unwrap_or_default())
}
pub(crate) async fn set_gossiped_timestamp(
self,
context: &Context,
timestamp: i64,
) -> Result<()> {
ensure!(
!self.is_special(),
"can not set gossiped timestamp for special chats"
);
info!(
context,
"Set gossiped_timestamp for chat {} to {}.", self, timestamp,
);
context
.sql
.execute(
"UPDATE chats SET gossiped_timestamp=? WHERE id=?;",
(timestamp, self),
)
.execute("DELETE FROM gossip_timestamp WHERE chat_id=?", (self,))
.await?;
Ok(())
}
@@ -1726,13 +1697,13 @@ impl Chat {
return Ok(Some(reason));
}
let reason = SecurejoinWait;
if !skip_fn(&reason) {
let (can_write, _) = self
if !skip_fn(&reason)
&& self
.check_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT)
.await?;
if !can_write {
return Ok(Some(reason));
}
.await?
> 0
{
return Ok(Some(reason));
}
Ok(None)
}
@@ -1744,18 +1715,17 @@ impl Chat {
Ok(self.why_cant_send(context).await?.is_none())
}
/// Returns if the chat can be sent to
/// and the remaining timeout for the 1:1 chat in-progress SecureJoin.
/// Returns the remaining timeout for the 1:1 chat in-progress SecureJoin.
///
/// If the timeout has expired, adds an info message with additional information;
/// the chat still cannot be sent to in this case. See also [`CantSendReason::SecurejoinWait`].
/// If the timeout has expired, adds an info message with additional information.
/// See also [`CantSendReason::SecurejoinWait`].
pub(crate) async fn check_securejoin_wait(
&self,
context: &Context,
timeout: u64,
) -> Result<(bool, u64)> {
) -> Result<u64> {
if self.typ != Chattype::Single || self.protected != ProtectionStatus::Unprotected {
return Ok((true, 0));
return Ok(0);
}
// chat is single and unprotected:
@@ -1779,11 +1749,10 @@ impl Chat {
)
.await?
else {
return Ok((true, 0));
return Ok(0);
};
if param == param_timeout {
return Ok((false, 0));
return Ok(0);
}
let now = time();
@@ -1793,10 +1762,9 @@ impl Chat {
.saturating_add(timeout.try_into()?)
.saturating_sub(now);
if timeout > 0 {
return Ok((false, timeout as u64));
return Ok(timeout as u64);
}
}
add_info_msg_with_cmd(
context,
self.id,
@@ -1811,8 +1779,8 @@ impl Chat {
None,
)
.await?;
Ok((false, 0))
context.emit_event(EventType::ChatModified(self.id));
Ok(0)
}
/// Checks if the user is part of a chat
@@ -1917,7 +1885,6 @@ impl Chat {
name: self.name.clone(),
archived: self.visibility == ChatVisibility::Archived,
param: self.param.to_string(),
gossiped_timestamp: self.id.get_gossiped_timestamp(context).await?,
is_sending_locations: self.is_sending_locations,
color: self.get_color(context).await?,
profile_image: self
@@ -2040,7 +2007,9 @@ impl Chat {
let mut to_id = 0;
let mut location_id = 0;
let new_rfc724_mid = create_outgoing_rfc724_mid();
if msg.rfc724_mid.is_empty() {
msg.rfc724_mid = create_outgoing_rfc724_mid();
}
if self.typ == Chattype::Single {
if let Some(id) = context
@@ -2138,7 +2107,7 @@ impl Chat {
if references_vec.is_empty() {
// As a fallback, use our Message-ID,
// same as in the case of top-level message.
new_references = new_rfc724_mid.clone();
new_references = msg.rfc724_mid.clone();
} else {
new_references = references_vec.join(" ");
}
@@ -2148,7 +2117,7 @@ impl Chat {
// This allows us to identify replies to our message even if
// email server such as Outlook changes `Message-ID:` header.
// MUAs usually keep the first Message-ID in `References:` header unchanged.
new_references = new_rfc724_mid.clone();
new_references = msg.rfc724_mid.clone();
}
// add independent location to database
@@ -2216,7 +2185,6 @@ impl Chat {
msg.chat_id = self.id;
msg.from_id = ContactId::SELF;
msg.rfc724_mid = new_rfc724_mid;
msg.timestamp_sort = timestamp;
// add message to the database
@@ -2460,9 +2428,6 @@ pub struct ChatInfo {
/// This is the string-serialised version of `Params` currently.
pub param: String,
/// Last time this client sent autocrypt gossip headers to this chat.
pub gossiped_timestamp: i64,
/// Whether this chat is currently sending location-stream messages.
pub is_sending_locations: bool,
@@ -2619,7 +2584,7 @@ pub(crate) async fn resume_securejoin_wait(context: &Context) -> Result<()> {
for chat_id in chat_ids {
let chat = Chat::load_from_db(context, chat_id).await?;
let (_, timeout) = chat
let timeout = chat
.check_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT)
.await?;
if timeout > 0 {
@@ -2978,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
);
}
}
}
@@ -3101,10 +3069,6 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
let now = smeared_time(context);
if rendered_msg.is_gossiped {
msg.chat_id.set_gossiped_timestamp(context, now).await?;
}
if rendered_msg.last_added_location_id.is_some() {
if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, now).await {
error!(context, "Failed to set kml sent_timestamp: {err:#}.");
@@ -3573,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)
}
@@ -3888,7 +3877,7 @@ pub(crate) async fn add_contact_to_chat_ex(
) -> Result<bool> {
ensure!(!chat_id.is_special(), "can not add member to special chats");
let contact = Contact::get_by_id(context, contact_id).await?;
let mut msg = Message::default();
let mut msg = Message::new(Viewtype::default());
chat_id.reset_gossiped_timestamp(context).await?;
@@ -4119,7 +4108,7 @@ pub async fn remove_contact_from_chat(
"Cannot remove special contact"
);
let mut msg = Message::default();
let mut msg = Message::new(Viewtype::default());
let chat = Chat::load_from_db(context, chat_id).await?;
if chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast {
@@ -4226,7 +4215,7 @@ async fn rename_ex(
ensure!(!chat_id.is_special(), "Invalid chat ID");
let chat = Chat::load_from_db(context, chat_id).await?;
let mut msg = Message::default();
let mut msg = Message::new(Viewtype::default());
if chat.typ == Chattype::Group
|| chat.typ == Chattype::Mailinglist
@@ -4354,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();
@@ -4389,9 +4378,11 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
msg.subject = "".to_string();
msg.state = MessageState::OutPending;
msg.rfc724_mid = create_outgoing_rfc724_mid();
let new_msg_id = chat
.prepare_msg_raw(context, &mut msg, None, curr_timestamp)
.await?;
curr_timestamp += 1;
if !create_send_msg_jobs(context, &mut msg).await?.is_empty() {
context.scheduler.interrupt_smtp().await;
@@ -4407,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
@@ -4428,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);
@@ -4470,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);
+175 -5
View File
@@ -1503,6 +1503,39 @@ async fn test_shall_attach_selfavatar() -> Result<()> {
Ok(())
}
/// Tests that profile data is attached to group leave messages. There are some pros and cons of
/// doing this, but at least we don't want to complicate the code with this special case.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_profile_data_on_group_leave() -> Result<()> {
let mut tcm = TestContextManager::new();
let t = &tcm.alice().await;
let chat_id = create_group_chat(t, ProtectionStatus::Unprotected, "foo").await?;
let (contact_id, _) = Contact::add_or_lookup(
t,
"",
&ContactAddress::new("foo@bar.org")?,
Origin::IncomingUnknownTo,
)
.await?;
add_contact_to_chat(t, chat_id, contact_id).await?;
send_text_msg(t, chat_id, "populate".to_string()).await?;
t.pop_sent_msg().await;
let file = t.dir.path().join("avatar.png");
let bytes = include_bytes!("../../test-data/image/avatar64x64.png");
tokio::fs::write(&file, bytes).await?;
t.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
.await?;
assert!(shall_attach_selfavatar(t, chat_id).await?);
remove_contact_from_chat(t, chat_id, ContactId::SELF).await?;
let sent_msg = t.pop_sent_msg().await;
assert!(sent_msg.payload().contains("Chat-User-Avatar"));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_mute_duration() {
let t = TestContext::new().await;
@@ -2007,20 +2040,28 @@ async fn test_sticker_forward() -> Result<()> {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_forward() -> Result<()> {
async fn test_forward_basic() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat = alice.create_chat(&bob).await;
let bob_chat = bob.create_chat(&alice).await;
let mut msg = Message::new_text("Hi Bob".to_owned());
let sent_msg = alice.send_msg(alice_chat.get_id(), &mut msg).await;
let mut alice_msg = Message::new_text("Hi Bob".to_owned());
let sent_msg = alice.send_msg(alice_chat.get_id(), &mut alice_msg).await;
let msg = bob.recv_msg(&sent_msg).await;
assert_eq!(alice_msg.rfc724_mid, msg.rfc724_mid);
forward_msgs(&bob, &[msg.id], bob_chat.get_id()).await?;
let forwarded_msg = bob.pop_sent_msg().await;
assert_eq!(bob_chat.id.get_msg_cnt(&bob).await?, 2);
assert_ne!(
forwarded_msg.load_from_db().await.rfc724_mid,
msg.rfc724_mid,
);
let msg_bob = Message::load_from_db(&bob, forwarded_msg.sender_msg_id).await?;
let msg = alice.recv_msg(&forwarded_msg).await;
assert_eq!(msg.rfc724_mid(), msg_bob.rfc724_mid());
assert_eq!(msg.get_text(), "Hi Bob");
assert!(msg.is_forwarded());
Ok(())
@@ -2245,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();
@@ -2516,6 +2591,7 @@ async fn test_broadcast() -> Result<()> {
// create two context, send two messages so both know the other
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let fiona = TestContext::new_fiona().await;
let chat_alice = alice.create_chat(&bob).await;
send_text_msg(&alice, chat_alice.id, "hi!".to_string()).await?;
@@ -2534,6 +2610,8 @@ async fn test_broadcast() -> Result<()> {
get_chat_contacts(&alice, chat_bob.id).await?.pop().unwrap(),
)
.await?;
let fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await;
add_contact_to_chat(&alice, broadcast_id, fiona_contact_id).await?;
set_chat_name(&alice, broadcast_id, "Broadcast list").await?;
{
let chat = Chat::load_from_db(&alice, broadcast_id).await?;
@@ -2548,7 +2626,10 @@ async fn test_broadcast() -> Result<()> {
{
let sent_msg = alice.pop_sent_msg().await;
assert!(!sent_msg.payload.contains("Chat-Group-Member-Timestamps:"));
let msg = bob.parse_msg(&sent_msg).await;
assert!(msg.was_encrypted());
assert!(!msg.header_exists(HeaderDef::ChatGroupMemberTimestamps));
assert!(!msg.header_exists(HeaderDef::AutocryptGossip));
let msg = bob.recv_msg(&sent_msg).await;
assert_eq!(msg.get_text(), "ola!");
assert_eq!(msg.subject, "Broadcast list");
@@ -2877,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;
@@ -3821,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.
@@ -4044,3 +4182,35 @@ async fn test_send_delete_request_no_encryption() -> Result<()> {
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, 1);
Ok(())
}
/// Tests that in multi-device setup
/// second device learns the key of a contact
/// via Autocrypt-Gossip in 1:1 chats.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_oneone_gossip() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let alice2 = &tcm.alice().await;
let bob = &tcm.bob().await;
tcm.section("Alice imports Bob's vCard and sends a message from the first device");
let alice_chat = alice.create_chat(bob).await;
let sent_msg = alice.send_text(alice_chat.id, "Hello Bob!").await;
tcm.section("Alice receives a copy on second device");
let rcvd_msg = alice2.recv_msg(&sent_msg).await;
assert_eq!(rcvd_msg.get_showpadlock(), true);
tcm.section("Alice sends a message from the second device");
let alice2_chat_id = rcvd_msg.chat_id;
let sent_msg2 = alice2
.send_text(alice2_chat_id, "Hello from second device!")
.await;
tcm.section("Bob receives a message from the second device");
let rcvd_msg2 = bob.recv_msg(&sent_msg2).await;
assert_eq!(rcvd_msg2.get_showpadlock(), true);
assert_eq!(rcvd_msg2.text, "Hello from second device!");
Ok(())
}
+1 -1
View File
@@ -322,7 +322,7 @@ impl Chatlist {
(chat_id, MessageState::OutDraft),
)
.await
.with_context(|| format!("failed to get msg ID for chat {}", chat_id))?;
.with_context(|| format!("failed to get msg ID for chat {chat_id}"))?;
ids.push((chat_id, msg_id));
}
Ok(Chatlist { ids })
+31 -10
View File
@@ -4,7 +4,7 @@ use std::env;
use std::path::Path;
use std::str::FromStr;
use anyhow::{ensure, Context as _, Result};
use anyhow::{bail, ensure, Context as _, Result};
use base64::Engine as _;
use deltachat_contact_tools::{addr_cmp, sanitize_single_line};
use serde::{Deserialize, Serialize};
@@ -13,10 +13,12 @@ use strum_macros::{AsRefStr, Display, EnumIter, EnumString};
use tokio::fs;
use crate::blob::BlobObject;
use crate::configure::EnteredLoginParam;
use crate::constants;
use crate::context::Context;
use crate::events::EventType;
use crate::log::LogExt;
use crate::login_param::ConfiguredLoginParam;
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::provider::{get_provider_by_id, Provider};
use crate::sync::{self, Sync::*, SyncData};
@@ -475,7 +477,10 @@ impl Config {
/// Whether the config option needs an IO scheduler restart to take effect.
pub(crate) fn needs_io_restart(&self) -> bool {
matches!(self, Config::OnlyFetchMvbox | Config::SentboxWatch)
matches!(
self,
Config::MvboxMove | Config::OnlyFetchMvbox | Config::SentboxWatch
)
}
}
@@ -522,21 +527,22 @@ impl Context {
// Default values
let val = match key {
Config::BccSelf => match Box::pin(self.is_chatmail()).await? {
false => Some("1"),
true => Some("0"),
false => Some("1".to_string()),
true => Some("0".to_string()),
},
Config::ConfiguredInboxFolder => Some("INBOX"),
Config::ConfiguredInboxFolder => Some("INBOX".to_string()),
Config::DeleteServerAfter => {
match !Box::pin(self.get_config_bool(Config::BccSelf)).await?
&& Box::pin(self.is_chatmail()).await?
{
true => Some("1"),
false => Some("0"),
true => Some("1".to_string()),
false => Some("0".to_string()),
}
}
_ => key.get_str("default"),
Config::Addr => self.get_config_opt(Config::ConfiguredAddr).await?,
_ => key.get_str("default").map(|s| s.to_string()),
};
Ok(val.map(|s| s.to_string()))
Ok(val)
}
/// Returns Some(T) if a value for the given key is set and was successfully parsed.
@@ -802,6 +808,19 @@ impl Context {
.set_raw_config(constants::DC_FOLDERS_CONFIGURED_KEY, None)
.await?;
}
Config::ConfiguredAddr => {
if self.is_configured().await? {
bail!("Cannot change ConfiguredAddr");
}
if let Some(addr) = value {
info!(self, "Creating a pseudo configured account which will not be able to send or receive messages. Only meant for tests!");
ConfiguredLoginParam::from_json(&format!(
r#"{{"addr":"{addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#
))?
.save_to_transports_table(self, &EnteredLoginParam::default())
.await?;
}
}
_ => {
self.sql.set_raw_config(key.as_ref(), value).await?;
}
@@ -888,6 +907,7 @@ impl Context {
/// primary address (if exists) as a secondary address.
///
/// This should only be used by test code and during configure.
#[cfg(test)] // AEAP is disabled, but there are still tests for it
pub(crate) async fn set_primary_self_addr(&self, primary_new: &str) -> Result<()> {
self.quota.write().await.take();
@@ -901,7 +921,8 @@ impl Context {
)
.await?;
self.set_config_internal(Config::ConfiguredAddr, Some(primary_new))
self.sql
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(primary_new))
.await?;
self.emit_event(EventType::ConnectivityChanged);
Ok(())
+51 -22
View File
@@ -16,7 +16,7 @@ pub(crate) mod server_params;
use anyhow::{bail, ensure, format_err, Context as _, Result};
use auto_mozilla::moz_autoconfigure;
use auto_outlook::outlk_autodiscover;
use deltachat_contact_tools::EmailAddress;
use deltachat_contact_tools::{addr_normalize, EmailAddress};
use futures::FutureExt;
use futures_lite::FutureExt as _;
use percent_encoding::utf8_percent_encode;
@@ -35,8 +35,7 @@ use crate::login_param::{
};
use crate::message::Message;
use crate::oauth2::get_oauth2_addr;
use crate::provider::{Protocol, Socket, UsernamePattern};
use crate::qr::set_account_from_qr;
use crate::provider::{Protocol, Provider, Socket, UsernamePattern};
use crate::smtp::Smtp;
use crate::sync::Sync::*;
use crate::tools::time;
@@ -63,17 +62,17 @@ macro_rules! progress {
impl Context {
/// Checks if the context is already configured.
pub async fn is_configured(&self) -> Result<bool> {
self.sql.get_raw_config_bool("configured").await
self.sql.exists("SELECT COUNT(*) FROM transports", ()).await
}
/// Configures this account with the currently provided parameters.
///
/// Deprecated since 2025-02; use `add_transport_from_qr()`
/// or `add_transport()` instead.
/// or `add_or_update_transport()` instead.
pub async fn configure(&self) -> Result<()> {
let param = EnteredLoginParam::load(self).await?;
let mut param = EnteredLoginParam::load(self).await?;
self.add_transport_inner(&param).await
self.add_transport_inner(&mut param).await
}
/// Configures a new email account using the provided parameters
@@ -105,7 +104,7 @@ impl Context {
/// from a server encoded in a QR code.
/// - [Self::list_transports()] to get a list of all configured transports.
/// - [Self::delete_transport()] to remove a transport.
pub async fn add_transport(&self, param: &EnteredLoginParam) -> Result<()> {
pub async fn add_or_update_transport(&self, param: &mut EnteredLoginParam) -> Result<()> {
self.stop_io().await;
let result = self.add_transport_inner(param).await;
if result.is_err() {
@@ -118,7 +117,7 @@ impl Context {
Ok(())
}
async fn add_transport_inner(&self, param: &EnteredLoginParam) -> Result<()> {
async fn add_transport_inner(&self, param: &mut EnteredLoginParam) -> Result<()> {
ensure!(
!self.scheduler.is_running().await,
"cannot configure, already running"
@@ -127,9 +126,12 @@ impl Context {
self.sql.is_open().await,
"cannot configure, database not opened."
);
param.addr = addr_normalize(&param.addr);
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
if self.is_configured().await? && !addr_cmp(&old_addr.unwrap_or_default(), &param.addr) {
bail!("Adding a new transport is not supported right now. Check back in a few months!");
let error_msg = "Changing your email address is not supported right now. Check back in a few months!";
progress!(self, 0, Some(error_msg.to_string()));
bail!(error_msg);
}
let cancel_channel = self.alloc_ongoing().await?;
@@ -156,12 +158,23 @@ impl Context {
/// Adds a new email account as a transport
/// using the server encoded in the QR code.
/// See [Self::add_transport].
/// See [Self::add_or_update_transport].
pub async fn add_transport_from_qr(&self, qr: &str) -> Result<()> {
self.stop_io().await;
// This code first sets the deprecated Config::Addr, Config::MailPw, etc.
// and then calls configure(), which loads them again.
// At some point, we will remove configure()
// and then simplify the code
// to directly create an EnteredLoginParam.
let result = async move {
set_account_from_qr(self, qr).await?;
match crate::qr::check_qr(self, qr).await? {
crate::qr::Qr::Account { .. } => crate::qr::set_account_from_qr(self, qr).await?,
crate::qr::Qr::Login { address, options } => {
crate::qr::configure_from_login_qr(self, &address, options).await?
}
_ => bail!("QR code does not contain account"),
}
self.configure().await?;
Ok(())
}
@@ -178,12 +191,24 @@ impl Context {
}
/// Returns the list of all email accounts that are used as a transport in the current profile.
/// Use [Self::add_transport()] to add or change a transport
/// Use [Self::add_or_update_transport()] to add or change a transport
/// and [Self::delete_transport()] to delete a transport.
pub async fn list_transports(&self) -> Result<Vec<EnteredLoginParam>> {
let param = EnteredLoginParam::load(self).await?;
let transports = self
.sql
.query_map(
"SELECT entered_param FROM transports",
(),
|row| row.get::<_, String>(0),
|rows| {
rows.flatten()
.map(|s| Ok(serde_json::from_str(&s)?))
.collect::<Result<Vec<EnteredLoginParam>>>()
},
)
.await?;
Ok(vec![param])
Ok(transports)
}
/// Removes the transport with the specified email address
@@ -197,20 +222,20 @@ impl Context {
info!(self, "Configure ...");
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
let configured_param = configure(self, param).await?;
let provider = configure(self, param).await?;
self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
.await?;
on_configure_completed(self, configured_param, old_addr).await?;
on_configure_completed(self, provider, old_addr).await?;
Ok(())
}
}
async fn on_configure_completed(
context: &Context,
param: ConfiguredLoginParam,
provider: Option<&'static Provider>,
old_addr: Option<String>,
) -> Result<()> {
if let Some(provider) = param.provider {
if let Some(provider) = provider {
if let Some(config_defaults) = provider.config_defaults {
for def in config_defaults {
if !context.config_exists(def.key).await? {
@@ -446,7 +471,7 @@ async fn get_configured_param(
Ok(configured_login_param)
}
async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<ConfiguredLoginParam> {
async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'static Provider>> {
progress!(ctx, 1);
let ctx2 = ctx.clone();
@@ -556,7 +581,11 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
}
}
configured_param.save_as_configured_params(ctx).await?;
let provider = configured_param.provider;
configured_param
.save_to_transports_table(ctx, param)
.await?;
ctx.set_config_internal(Config::ConfiguredTimestamp, Some(&time().to_string()))
.await?;
@@ -572,7 +601,7 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
ctx.sql.set_raw_config_bool("configured", true).await?;
ctx.emit_event(EventType::AccountsItemChanged);
Ok(configured_param)
Ok(provider)
}
/// Retrieve available autoconfigurations.
-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
+13 -8
View File
@@ -25,7 +25,7 @@ use crate::blob::BlobObject;
use crate::chat::{ChatId, ChatIdBlocked, ProtectionStatus};
use crate::color::str_to_color;
use crate::config::Config;
use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY};
use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF};
use crate::context::Context;
use crate::events::EventType;
use crate::key::{load_self_public_key, DcKey, SignedPublicKey};
@@ -287,6 +287,7 @@ pub async fn make_vcard(context: &Context, contacts: &[ContactId]) -> Result<Str
authname: c.authname,
key,
profile_image,
biography: Some(c.status).filter(|s| !s.is_empty()),
// Use the current time to not reveal our or contact's online time.
timestamp: Ok(now),
});
@@ -423,6 +424,14 @@ async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Resu
);
}
}
if let Some(biography) = &contact.biography {
if let Err(e) = set_status(context, id, biography.to_owned(), false, false).await {
warn!(
context,
"import_vcard_contact: Could not set biography for {}: {e:#}.", contact.addr
);
}
}
Ok(id)
}
@@ -1041,9 +1050,8 @@ impl Contact {
///
/// `listflags` is a combination of flags:
/// - if the flag DC_GCL_ADD_SELF is set, SELF is added to the list unless filtered by other parameters
/// - if the flag DC_GCL_VERIFIED_ONLY is set, only verified contacts are returned.
/// if DC_GCL_VERIFIED_ONLY is not set, verified and unverified contacts are returned.
/// `query` is a string to filter the list.
///
/// `query` is a string to filter the list.
pub async fn get_all(
context: &Context,
listflags: u32,
@@ -1056,14 +1064,13 @@ impl Contact {
.collect::<HashSet<_>>();
let mut add_self = false;
let mut ret = Vec::new();
let flag_verified_only = (listflags & DC_GCL_VERIFIED_ONLY) != 0;
let flag_add_self = (listflags & DC_GCL_ADD_SELF) != 0;
let minimal_origin = if context.get_config_bool(Config::Bot).await? {
Origin::Unknown
} else {
Origin::IncomingReplyTo
};
if flag_verified_only || query.is_some() {
if query.is_some() {
let s3str_like_cmd = format!("%{}%", query.unwrap_or(""));
context
.sql
@@ -1074,14 +1081,12 @@ impl Contact {
AND c.origin>=? \
AND c.blocked=0 \
AND (iif(c.name='',c.authname,c.name) LIKE ? OR c.addr LIKE ?) \
AND (1=? OR LENGTH(ps.verified_key_fingerprint)!=0) \
ORDER BY c.last_seen DESC, c.id DESC;",
(
ContactId::LAST_SPECIAL,
minimal_origin,
&s3str_like_cmd,
&s3str_like_cmd,
if flag_verified_only { 0i32 } else { 1i32 },
),
|row| {
let id: ContactId = row.get(0)?;
+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(())
+16 -30
View File
@@ -4,19 +4,19 @@ use std::collections::{BTreeMap, HashMap};
use std::ffi::OsString;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use anyhow::{bail, ensure, Context as _, Result};
use anyhow::{Context as _, Result, bail, ensure};
use async_channel::{self as channel, Receiver, Sender};
use pgp::composed::SignedPublicKey;
use pgp::types::PublicKeyTrait;
use pgp::SignedPublicKey;
use ratelimit::Ratelimit;
use tokio::sync::{Mutex, Notify, RwLock};
use crate::aheader::EncryptPreference;
use crate::chat::{get_chat_cnt, ChatId, ProtectionStatus};
use crate::chat::{ChatId, ProtectionStatus, get_chat_cnt};
use crate::chatlist_events;
use crate::config::Config;
use crate::constants::{
@@ -27,7 +27,7 @@ use crate::debug_logging::DebugLogging;
use crate::download::DownloadState;
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::imap::{FolderMeaning, Imap, ServerMetadata};
use crate::key::{load_self_public_key, load_self_secret_key, DcKey as _};
use crate::key::{DcKey as _, load_self_public_key, load_self_secret_key};
use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam};
use crate::message::{self, Message, MessageState, MsgId};
use crate::param::{Param, Params};
@@ -35,7 +35,7 @@ use crate::peer_channels::Iroh;
use crate::peerstate::Peerstate;
use crate::push::PushSubscriber;
use crate::quota::QuotaInfo;
use crate::scheduler::{convert_folder_meaning, SchedulerState};
use crate::scheduler::{SchedulerState, convert_folder_meaning};
use crate::sql::Sql;
use crate::stock_str::StockStrings;
use crate::timesmearing::SmearedTimestamp;
@@ -292,7 +292,7 @@ pub struct InnerContext {
pub(crate) push_subscribed: AtomicBool,
/// Iroh for realtime peer channels.
pub(crate) iroh: Arc<RwLock<Option<Iroh>>>,
pub(crate) iroh: Arc<Iroh>,
}
/// The state of ongoing process.
@@ -450,7 +450,7 @@ impl Context {
debug_logging: std::sync::RwLock::new(None),
push_subscriber,
push_subscribed: AtomicBool::new(false),
iroh: Arc::new(RwLock::new(None)),
iroh: Default::default(),
};
let ctx = Context {
@@ -486,19 +486,7 @@ impl Context {
/// Stops the IO scheduler.
pub async fn stop_io(&self) {
self.scheduler.stop(self).await;
if let Some(iroh) = self.iroh.write().await.take() {
// Close all QUIC connections.
// Spawn into a separate task,
// because Iroh calls `wait_idle()` internally
// and it may take time, especially if the network
// has become unavailable.
tokio::spawn(async move {
// We do not log the error because we do not want the task
// to hold the reference to Context.
let _ = tokio::time::timeout(Duration::from_secs(60), iroh.close()).await;
});
}
self.iroh.close();
}
/// Restarts the IO scheduler if it was running before
@@ -509,9 +497,7 @@ impl Context {
/// Indicate that the network likely has come back.
pub async fn maybe_network(&self) {
if let Some(ref iroh) = *self.iroh.read().await {
iroh.network_change().await;
}
self.iroh.network_change().await;
self.scheduler.maybe_network().await;
}
@@ -1061,21 +1047,21 @@ impl Context {
)
.await?
.unwrap_or_default();
res += &format!("num_msgs {}\n", num_msgs);
res += &format!("num_msgs {num_msgs}\n");
let num_chats: u32 = self
.sql
.query_get_value("SELECT COUNT(*) FROM chats WHERE id>9 AND blocked!=1", ())
.await?
.unwrap_or_default();
res += &format!("num_chats {}\n", num_chats);
res += &format!("num_chats {num_chats}\n");
let db_size = tokio::fs::metadata(&self.sql.dbfile).await?.len();
res += &format!("db_size_bytes {}\n", db_size);
res += &format!("db_size_bytes {db_size}\n");
let secret_key = &load_self_secret_key(self).await?.primary_key;
let key_created = secret_key.created_at().timestamp();
res += &format!("key_created {}\n", key_created);
let key_created = secret_key.public_key().created_at().timestamp();
res += &format!("key_created {key_created}\n");
// how many of the chats active in the last months are:
// - protected
@@ -1155,7 +1141,7 @@ impl Context {
id
}
};
res += &format!("self_reporting_id {}", self_reporting_id);
res += &format!("self_reporting_id {self_reporting_id}");
Ok(res)
}
+4 -4
View File
@@ -15,10 +15,10 @@ use crate::pgp;
/// Tries to decrypt a message, but only if it is structured as an Autocrypt message.
///
/// If successful and the message is encrypted, returns decrypted body.
pub fn try_decrypt(
mail: &ParsedMail<'_>,
private_keyring: &[SignedSecretKey],
) -> Result<Option<::pgp::composed::Message>> {
pub fn try_decrypt<'a>(
mail: &'a ParsedMail<'a>,
private_keyring: &'a [SignedSecretKey],
) -> Result<Option<::pgp::composed::Message<'static>>> {
let Some(encrypted_data_part) = get_encrypted_mime(mail) else {
return Ok(None);
};
+3 -4
View File
@@ -143,7 +143,7 @@ impl EncryptHelper {
let cursor = Cursor::new(&mut raw_message);
mail_to_encrypt.clone().write_part(cursor).ok();
let ctext = pgp::pk_encrypt(&raw_message, keyring, Some(sign_key), compress).await?;
let ctext = pgp::pk_encrypt(raw_message, keyring, Some(sign_key), compress).await?;
Ok(ctext)
}
@@ -153,9 +153,8 @@ impl EncryptHelper {
pub async fn sign(self, context: &Context, mail: &MimePart<'static>) -> Result<String> {
let sign_key = load_self_secret_key(context).await?;
let mut buffer = Vec::new();
let cursor = Cursor::new(&mut buffer);
mail.clone().write_part(cursor).ok();
let signature = pgp::pk_calc_signature(&buffer, &sign_key)?;
mail.clone().write_part(&mut buffer)?;
let signature = pgp::pk_calc_signature(buffer, &sign_key)?;
Ok(signature)
}
}
+7 -9
View File
@@ -285,9 +285,7 @@ impl Imap {
Ok(imap)
}
/// Connects or reconnects if needed.
///
/// It is safe to call this function if already connected, actions are performed only as needed.
/// Connects to IMAP server and returns a new IMAP session.
///
/// Calling this function is not enough to perform IMAP operations. Use [`Imap::prepare`]
/// instead if you are going to actually use connection rather than trying connection
@@ -326,7 +324,7 @@ impl Imap {
}
}
info!(context, "Connecting to IMAP server");
info!(context, "Connecting to IMAP server.");
self.connectivity.set_connecting(context).await;
self.conn_last_try = tools::Time::now();
@@ -411,7 +409,7 @@ impl Imap {
lp.user
)));
self.connectivity.set_preparing(context).await;
info!(context, "Successfully logged into IMAP server");
info!(context, "Successfully logged into IMAP server.");
return Ok(session);
}
@@ -459,10 +457,10 @@ impl Imap {
Err(first_error.unwrap_or_else(|| format_err!("No IMAP connection candidates provided")))
}
/// Prepare for IMAP operation.
/// Prepare a new IMAP session.
///
/// Ensure that IMAP client is connected, folders are created and IMAP capabilities are
/// determined.
/// This creates a new IMAP connection and ensures
/// that folders are created and IMAP capabilities are determined.
pub(crate) async fn prepare(&mut self, context: &Context) -> Result<Session> {
let configuring = false;
let mut session = match self.connect(context, configuring).await {
@@ -1048,7 +1046,7 @@ impl Session {
// Expunge folder if needed, e.g. if some jobs have
// deleted messages on the server.
if let Err(err) = self.maybe_close_folder(context).await {
warn!(context, "failed to close folder: {:?}", err);
warn!(context, "Failed to close folder: {err:#}.");
}
Ok(())
+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();
+16 -9
View File
@@ -17,16 +17,24 @@ impl Imap {
session: &mut Session,
) -> Result<bool> {
// First of all, debounce to once per minute:
let mut last_scan = context.last_full_folder_scan.lock().await;
if let Some(last_scan) = *last_scan {
let elapsed_secs = time_elapsed(&last_scan).as_secs();
let debounce_secs = context
.get_config_u64(Config::ScanAllFoldersDebounceSecs)
.await?;
{
let mut last_scan = context.last_full_folder_scan.lock().await;
if let Some(last_scan) = *last_scan {
let elapsed_secs = time_elapsed(&last_scan).as_secs();
let debounce_secs = context
.get_config_u64(Config::ScanAllFoldersDebounceSecs)
.await?;
if elapsed_secs < debounce_secs {
return Ok(false);
if elapsed_secs < debounce_secs {
return Ok(false);
}
}
// Update the timestamp before scanning the folders
// to avoid holding the lock for too long.
// This means next scan is delayed even if
// the current one fails.
last_scan.replace(tools::Time::now());
}
info!(context, "Starting full folder scan");
@@ -94,7 +102,6 @@ impl Imap {
}
info!(context, "Found folders: {folder_names:?}.");
last_scan.replace(tools::Time::now());
Ok(true)
}
}
+9 -8
View File
@@ -33,10 +33,10 @@ impl ImapSession {
pub(super) async fn maybe_close_folder(&mut self, context: &Context) -> anyhow::Result<()> {
if let Some(folder) = &self.selected_folder {
if self.selected_folder_needs_expunge {
info!(context, "Expunge messages in \"{}\".", folder);
info!(context, "Expunge messages in {folder:?}.");
self.close().await.context("IMAP close/expunge failed")?;
info!(context, "close/expunge succeeded");
info!(context, "Close/expunge succeeded.");
self.selected_folder = None;
self.selected_folder_needs_expunge = false;
self.new_mail = false;
@@ -73,6 +73,7 @@ impl ImapSession {
match res {
Ok(mailbox) => {
info!(context, "Selected folder {folder:?}.");
self.selected_folder = Some(folder.to_string());
self.selected_mailbox = Some(mailbox);
Ok(NewlySelected::Yes)
@@ -94,7 +95,7 @@ impl ImapSession {
Ok(newly_selected) => Ok(newly_selected),
Err(err) => match err {
Error::NoFolder(..) => {
info!(context, "Failed to select folder {} because it does not exist, trying to create it.", folder);
info!(context, "Failed to select folder {folder:?} because it does not exist, trying to create it.");
let create_res = self.create(folder).await;
if let Err(ref err) = create_res {
info!(context, "Couldn't select folder, then create() failed: {err:#}.");
@@ -129,7 +130,7 @@ impl ImapSession {
let newly_selected = if create {
self.select_or_create_folder(context, folder)
.await
.with_context(|| format!("failed to select or create folder {folder}"))?
.with_context(|| format!("Failed to select or create folder {folder:?}"))?
} else {
match self.select_folder(context, folder).await {
Ok(newly_selected) => newly_selected,
@@ -137,7 +138,7 @@ impl ImapSession {
Error::NoFolder(..) => return Ok(false),
_ => {
return Err(err)
.with_context(|| format!("failed to select folder {folder}"))?
.with_context(|| format!("Failed to select folder {folder:?}"))?
}
},
}
@@ -145,14 +146,14 @@ impl ImapSession {
let mailbox = self
.selected_mailbox
.as_mut()
.with_context(|| format!("No mailbox selected, folder: {folder}"))?;
.with_context(|| format!("No mailbox selected, folder: {folder:?}"))?;
let old_uid_validity = get_uidvalidity(context, folder)
.await
.with_context(|| format!("failed to get old UID validity for folder {folder}"))?;
.with_context(|| format!("Failed to get old UID validity for folder {folder:?}"))?;
let old_uid_next = get_uid_next(context, folder)
.await
.with_context(|| format!("failed to get old UID NEXT for folder {folder}"))?;
.with_context(|| format!("Failed to get old UID NEXT for folder {folder:?}"))?;
let new_uid_validity = mailbox
.uid_validity
+6 -2
View File
@@ -4,7 +4,6 @@ use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use ::pgp::types::PublicKeyTrait;
use anyhow::{bail, ensure, format_err, Context as _, Result};
use futures::TryStreamExt;
use futures_lite::FutureExt;
@@ -32,6 +31,7 @@ use crate::tools::{
mod key_transfer;
mod transfer;
use ::pgp::types::KeyDetails;
pub use key_transfer::{continue_key_transfer, initiate_key_transfer};
pub use transfer::{get_backup, BackupProvider};
@@ -173,7 +173,11 @@ async fn set_self_key(context: &Context, armored: &str) -> Result<()> {
};
key::store_self_keypair(context, &keypair).await?;
info!(context, "stored self key: {:?}", keypair.secret.key_id());
info!(
context,
"stored self key: {:?}",
keypair.secret.public_key().key_id()
);
Ok(())
}
+16 -19
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};
@@ -32,10 +34,7 @@ pub async fn initiate_key_transfer(context: &Context) -> Result<String> {
)?;
let chat_id = ChatId::create_for_contact(context, ContactId::SELF).await?;
let mut msg = Message {
viewtype: Viewtype::File,
..Default::default()
};
let mut msg = Message::new(Viewtype::File);
msg.param.set(Param::File, setup_file_blob.as_name());
msg.param
.set(Param::Filename, "autocrypt-setup-message.html");
@@ -74,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?;
@@ -99,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");
@@ -158,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> {
@@ -261,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();
@@ -281,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)]
+6 -1
View File
@@ -95,6 +95,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 +302,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?;
+28 -8
View File
@@ -10,7 +10,7 @@ use deltachat_contact_tools::EmailAddress;
use pgp::composed::Deserializable;
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
use pgp::ser::Serialize;
use pgp::types::{PublicKeyTrait, SecretKeyTrait};
use pgp::types::{KeyDetails, KeyId, Password};
use rand::thread_rng;
use tokio::runtime::Handle;
@@ -24,7 +24,7 @@ use crate::tools::{self, time_elapsed};
/// This trait is implemented for rPGP's [SignedPublicKey] and
/// [SignedSecretKey] types and makes working with them a little
/// easier in the deltachat world.
pub(crate) trait DcKey: Serialize + Deserializable + PublicKeyTrait + Clone {
pub(crate) trait DcKey: Serialize + Deserializable + Clone {
/// Create a key from some bytes.
fn from_slice(bytes: &[u8]) -> Result<Self> {
let res = <Self as Deserializable>::from_bytes(Cursor::new(bytes));
@@ -78,7 +78,7 @@ pub(crate) trait DcKey: Serialize + Deserializable + PublicKeyTrait + Clone {
let bytes = data.as_bytes();
let res = Self::from_armor_single(Cursor::new(bytes));
let (key, headers) = match res {
Err(pgp::errors::Error::NoMatchingPacket) => match Self::is_private() {
Err(pgp::errors::Error::NoMatchingPacket { .. }) => match Self::is_private() {
true => bail!("No private key packet found"),
false => bail!("No public key packet found"),
},
@@ -123,11 +123,10 @@ pub(crate) trait DcKey: Serialize + Deserializable + PublicKeyTrait + Clone {
fn to_asc(&self, header: Option<(&str, &str)>) -> String;
/// The fingerprint for the key.
fn dc_fingerprint(&self) -> Fingerprint {
PublicKeyTrait::fingerprint(self).into()
}
fn dc_fingerprint(&self) -> Fingerprint;
fn is_private() -> bool;
fn key_id(&self) -> KeyId;
}
pub(crate) async fn load_self_public_key(context: &Context) -> Result<SignedPublicKey> {
@@ -230,6 +229,14 @@ impl DcKey for SignedPublicKey {
fn is_private() -> bool {
false
}
fn dc_fingerprint(&self) -> Fingerprint {
self.fingerprint().into()
}
fn key_id(&self) -> KeyId {
KeyDetails::key_id(self)
}
}
impl DcKey for SignedSecretKey {
@@ -249,6 +256,14 @@ impl DcKey for SignedSecretKey {
fn is_private() -> bool {
true
}
fn dc_fingerprint(&self) -> Fingerprint {
self.fingerprint().into()
}
fn key_id(&self) -> KeyId {
KeyDetails::key_id(&**self)
}
}
/// Deltachat extension trait for secret keys.
@@ -262,9 +277,14 @@ pub(crate) trait DcSecretKey {
impl DcSecretKey for SignedSecretKey {
fn split_public_key(&self) -> Result<SignedPublicKey> {
self.verify()?;
let unsigned_pubkey = SecretKeyTrait::public_key(self);
let unsigned_pubkey = self.public_key();
let mut rng = thread_rng();
let signed_pubkey = unsigned_pubkey.sign(&mut rng, self, || "".into())?;
let signed_pubkey = unsigned_pubkey.sign(
&mut rng,
&self.primary_key,
self.primary_key.public_key(),
&Password::empty(),
)?;
Ok(signed_pubkey)
}
}
+237 -99
View File
@@ -2,26 +2,38 @@
use std::fmt;
use anyhow::{format_err, Context as _, Result};
use deltachat_contact_tools::EmailAddress;
use anyhow::{bail, ensure, format_err, Context as _, Result};
use deltachat_contact_tools::{addr_cmp, addr_normalize, EmailAddress};
use num_traits::ToPrimitive as _;
use serde::{Deserialize, Serialize};
use crate::config::Config;
use crate::configure::server_params::{expand_param_vector, ServerParams};
use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2};
use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2};
use crate::context::Context;
use crate::net::load_connection_timestamp;
pub use crate::net::proxy::ProxyConfig;
pub use crate::provider::Socket;
use crate::provider::{Protocol, Provider, UsernamePattern};
use crate::provider::{get_provider_by_id, Protocol, Provider, UsernamePattern};
use crate::sql::Sql;
use crate::tools::ToOption;
/// User-entered setting for certificate checks.
///
/// Should be saved into `imap_certificate_checks` before running configuration.
#[derive(Copy, Clone, Debug, Default, Display, FromPrimitive, ToPrimitive, PartialEq, Eq)]
#[derive(
Copy,
Clone,
Debug,
Default,
Display,
FromPrimitive,
ToPrimitive,
PartialEq,
Eq,
Serialize,
Deserialize,
)]
#[repr(u32)]
#[strum(serialize_all = "snake_case")]
pub enum EnteredCertificateChecks {
@@ -44,7 +56,9 @@ pub enum EnteredCertificateChecks {
}
/// Values saved into `imap_certificate_checks`.
#[derive(Copy, Clone, Debug, Display, FromPrimitive, ToPrimitive, PartialEq, Eq)]
#[derive(
Copy, Clone, Debug, Display, FromPrimitive, ToPrimitive, PartialEq, Eq, Serialize, Deserialize,
)]
#[repr(u32)]
#[strum(serialize_all = "snake_case")]
pub(crate) enum ConfiguredCertificateChecks {
@@ -81,7 +95,7 @@ pub(crate) enum ConfiguredCertificateChecks {
}
/// Login parameters for a single server, either IMAP or SMTP
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EnteredServerLoginParam {
/// Server hostname or IP address.
pub server: String,
@@ -104,7 +118,7 @@ pub struct EnteredServerLoginParam {
}
/// Login parameters entered by the user.
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EnteredLoginParam {
/// Email address.
pub addr: String,
@@ -451,6 +465,22 @@ pub(crate) struct ConfiguredLoginParam {
pub oauth2: bool,
}
/// The representation of ConfiguredLoginParam in the database,
/// saved as Json.
#[derive(Debug, Serialize, Deserialize)]
struct ConfiguredLoginParamJson {
pub addr: String,
pub imap: Vec<ConfiguredServerLoginParam>,
pub imap_user: String,
pub imap_password: String,
pub smtp: Vec<ConfiguredServerLoginParam>,
pub smtp_user: String,
pub smtp_password: String,
pub provider_id: Option<String>,
pub certificate_checks: ConfiguredCertificateChecks,
pub oauth2: bool,
}
impl fmt::Display for ConfiguredLoginParam {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let addr = &self.addr;
@@ -487,6 +517,26 @@ impl ConfiguredLoginParam {
///
/// Returns `None` if account is not configured.
pub(crate) async fn load(context: &Context) -> Result<Option<Self>> {
let Some(self_addr) = context.get_config(Config::ConfiguredAddr).await? else {
return Ok(None);
};
let json: Option<String> = context
.sql
.query_get_value(
"SELECT configured_param FROM transports WHERE addr=?",
(&self_addr,),
)
.await?;
if let Some(json) = json {
Ok(Some(Self::from_json(&json)?))
} else {
bail!("Self address {self_addr} doesn't have a corresponding transport");
}
}
/// Loads legacy configured param. Only used for tests and the migration.
pub(crate) async fn load_legacy(context: &Context) -> Result<Option<Self>> {
if !context.get_config_bool(Config::Configured).await? {
return Ok(None);
}
@@ -753,84 +803,82 @@ impl ConfiguredLoginParam {
}))
}
/// Save this loginparam to the database.
pub(crate) async fn save_as_configured_params(&self, context: &Context) -> Result<()> {
context.set_primary_self_addr(&self.addr).await?;
pub(crate) async fn save_to_transports_table(
self,
context: &Context,
entered_param: &EnteredLoginParam,
) -> Result<()> {
let addr = addr_normalize(&self.addr);
let configured_addr = context.get_config(Config::ConfiguredAddr).await?;
if let Some(configured_addr) = configured_addr {
ensure!(
addr_cmp(&configured_addr, &addr,),
"Adding a second transport is not supported right now."
);
}
context
.set_config(
Config::ConfiguredImapServers,
Some(&serde_json::to_string(&self.imap)?),
)
.await?;
context
.set_config(
Config::ConfiguredSmtpServers,
Some(&serde_json::to_string(&self.smtp)?),
)
.await?;
context
.set_config(Config::ConfiguredMailUser, Some(&self.imap_user))
.await?;
context
.set_config(Config::ConfiguredMailPw, Some(&self.imap_password))
.await?;
context
.set_config(Config::ConfiguredSendUser, Some(&self.smtp_user))
.await?;
context
.set_config(Config::ConfiguredSendPw, Some(&self.smtp_password))
.await?;
context
.set_config_u32(
Config::ConfiguredImapCertificateChecks,
self.certificate_checks as u32,
)
.await?;
context
.set_config_u32(
Config::ConfiguredSmtpCertificateChecks,
self.certificate_checks as u32,
)
.await?;
// Remove legacy settings.
context
.set_config(Config::ConfiguredMailServer, None)
.await?;
context.set_config(Config::ConfiguredMailPort, None).await?;
context
.set_config(Config::ConfiguredMailSecurity, None)
.await?;
context
.set_config(Config::ConfiguredSendServer, None)
.await?;
context.set_config(Config::ConfiguredSendPort, None).await?;
context
.set_config(Config::ConfiguredSendSecurity, None)
.await?;
let server_flags = match self.oauth2 {
true => DC_LP_AUTH_OAUTH2,
false => DC_LP_AUTH_NORMAL,
};
context
.set_config_u32(Config::ConfiguredServerFlags, server_flags as u32)
.await?;
context
.set_config(
Config::ConfiguredProvider,
.sql
.set_raw_config(
Config::ConfiguredProvider.as_ref(),
self.provider.map(|provider| provider.id),
)
.await?;
context
.sql
.execute(
"INSERT INTO transports (addr, entered_param, configured_param)
VALUES (?, ?, ?)
ON CONFLICT (addr)
DO UPDATE SET entered_param=excluded.entered_param, configured_param=excluded.configured_param",
(
self.addr.clone(),
serde_json::to_string(entered_param)?,
self.into_json()?,
),
)
.await?;
context
.sql
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr))
.await?;
Ok(())
}
pub(crate) fn from_json(json: &str) -> Result<Self> {
let json: ConfiguredLoginParamJson = serde_json::from_str(json)?;
let provider = json.provider_id.and_then(|id| get_provider_by_id(&id));
Ok(ConfiguredLoginParam {
addr: json.addr,
imap: json.imap,
imap_user: json.imap_user,
imap_password: json.imap_password,
smtp: json.smtp,
smtp_user: json.smtp_user,
smtp_password: json.smtp_password,
provider,
certificate_checks: json.certificate_checks,
oauth2: json.oauth2,
})
}
pub(crate) fn into_json(self) -> Result<String> {
let json = ConfiguredLoginParamJson {
addr: self.addr,
imap: self.imap,
imap_user: self.imap_user,
imap_password: self.imap_password,
smtp: self.smtp,
smtp_user: self.smtp_user,
smtp_password: self.smtp_password,
provider_id: self.provider.map(|p| p.id.to_string()),
certificate_checks: self.certificate_checks,
oauth2: self.oauth2,
};
Ok(serde_json::to_string(&json)?)
}
pub(crate) fn strict_tls(&self, connected_through_proxy: bool) -> bool {
let provider_strict_tls = self.provider.map(|provider| provider.opt.strict_tls);
match self.certificate_checks {
@@ -848,8 +896,10 @@ impl ConfiguredLoginParam {
#[cfg(test)]
mod tests {
use super::*;
use crate::log::LogExt as _;
use crate::provider::get_provider_by_id;
use crate::test_utils::TestContext;
use pretty_assertions::assert_eq;
#[test]
fn test_certificate_checks_display() {
@@ -961,18 +1011,33 @@ mod tests {
oauth2: false,
};
param.save_as_configured_params(&t).await?;
param
.clone()
.save_to_transports_table(&t, &EnteredLoginParam::default())
.await?;
let expected_param = r#"{"addr":"alice@example.org","imap":[{"connection":{"host":"imap.example.com","port":123,"security":"Starttls"},"user":"alice"}],"imap_user":"","imap_password":"foo","smtp":[{"connection":{"host":"smtp.example.com","port":456,"security":"Tls"},"user":"alice@example.org"}],"smtp_user":"","smtp_password":"bar","provider_id":null,"certificate_checks":"Strict","oauth2":false}"#;
assert_eq!(
t.get_config(Config::ConfiguredImapServers).await?.unwrap(),
r#"[{"connection":{"host":"imap.example.com","port":123,"security":"Starttls"},"user":"alice"}]"#
t.sql
.query_get_value::<String>("SELECT configured_param FROM transports", ())
.await?
.unwrap(),
expected_param
);
t.set_config(Config::Configured, Some("1")).await?;
assert_eq!(t.is_configured().await?, true);
let loaded = ConfiguredLoginParam::load(&t).await?.unwrap();
assert_eq!(param, loaded);
// Test that we don't panic on unknown ConfiguredImapCertificateChecks values.
// Legacy ConfiguredImapCertificateChecks config is ignored
t.set_config(Config::ConfiguredImapCertificateChecks, Some("999"))
.await?;
assert!(ConfiguredLoginParam::load(&t).await.is_ok());
// Test that we don't panic on unknown ConfiguredImapCertificateChecks values.
let wrong_param = expected_param.replace("Strict", "Stricct");
assert_ne!(expected_param, wrong_param);
t.sql
.execute("UPDATE transports SET configured_param=?", (wrong_param,))
.await?;
assert!(ConfiguredLoginParam::load(&t).await.is_err());
Ok(())
@@ -989,7 +1054,8 @@ mod tests {
t.set_config(Config::Configured, Some("1")).await?;
t.set_config(Config::ConfiguredProvider, Some("posteo"))
.await?;
t.set_config(Config::ConfiguredAddr, Some("alice@posteo.at"))
t.sql
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some("alice@posteo.at"))
.await?;
t.set_config(Config::ConfiguredMailServer, Some("posteo.de"))
.await?;
@@ -1063,12 +1129,68 @@ mod tests {
oauth2: false,
};
let loaded = ConfiguredLoginParam::load_legacy(&t).await?.unwrap();
assert_eq!(loaded, param);
migrate_configured_login_param(&t).await;
let loaded = ConfiguredLoginParam::load(&t).await?.unwrap();
assert_eq!(loaded, param);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_empty_server_list_legacy() -> Result<()> {
// Find a provider that does not have server list set.
//
// There is at least one such provider in the provider database.
let (domain, provider) = crate::provider::data::PROVIDER_DATA
.iter()
.find(|(_domain, provider)| provider.server.is_empty())
.unwrap();
let t = TestContext::new().await;
let addr = format!("alice@{domain}");
t.set_config(Config::Configured, Some("1")).await?;
t.set_config(Config::ConfiguredProvider, Some(provider.id))
.await?;
t.sql
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr))
.await?;
t.set_config(Config::ConfiguredMailPw, Some("foobarbaz"))
.await?;
t.set_config(Config::ConfiguredImapCertificateChecks, Some("1"))
.await?; // Strict
t.set_config(Config::ConfiguredSendPw, Some("foobarbaz"))
.await?;
t.set_config(Config::ConfiguredSmtpCertificateChecks, Some("1"))
.await?; // Strict
t.set_config(Config::ConfiguredServerFlags, Some("0"))
.await?;
let loaded = ConfiguredLoginParam::load_legacy(&t).await?.unwrap();
assert_eq!(loaded.provider, Some(*provider));
assert_eq!(loaded.imap.is_empty(), false);
assert_eq!(loaded.smtp.is_empty(), false);
migrate_configured_login_param(&t).await;
let loaded = ConfiguredLoginParam::load(&t).await?.unwrap();
assert_eq!(loaded.provider, Some(*provider));
assert_eq!(loaded.imap.is_empty(), false);
assert_eq!(loaded.smtp.is_empty(), false);
Ok(())
}
async fn migrate_configured_login_param(t: &TestContext) {
t.sql.execute("DROP TABLE transports;", ()).await.unwrap();
t.sql.set_raw_config_int("dbversion", 130).await.unwrap();
t.sql.run_migrations(t).await.log_err(t).ok();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_empty_server_list() -> Result<()> {
// Find a provider that does not have server list set.
@@ -1083,25 +1205,41 @@ mod tests {
let addr = format!("alice@{domain}");
t.set_config(Config::Configured, Some("1")).await?;
t.set_config(Config::ConfiguredProvider, Some(provider.id))
.await?;
t.set_config(Config::ConfiguredAddr, Some(&addr)).await?;
t.set_config(Config::ConfiguredMailPw, Some("foobarbaz"))
.await?;
t.set_config(Config::ConfiguredImapCertificateChecks, Some("1"))
.await?; // Strict
t.set_config(Config::ConfiguredSendPw, Some("foobarbaz"))
.await?;
t.set_config(Config::ConfiguredSmtpCertificateChecks, Some("1"))
.await?; // Strict
t.set_config(Config::ConfiguredServerFlags, Some("0"))
.await?;
ConfiguredLoginParam {
addr: addr.clone(),
imap: vec![ConfiguredServerLoginParam {
connection: ConnectionCandidate {
host: "example.org".to_string(),
port: 100,
security: ConnectionSecurity::Tls,
},
user: addr.clone(),
}],
imap_user: addr.clone(),
imap_password: "foobarbaz".to_string(),
smtp: vec![ConfiguredServerLoginParam {
connection: ConnectionCandidate {
host: "example.org".to_string(),
port: 100,
security: ConnectionSecurity::Tls,
},
user: addr.clone(),
}],
smtp_user: addr.clone(),
smtp_password: "foobarbaz".to_string(),
provider: Some(provider),
certificate_checks: ConfiguredCertificateChecks::Automatic,
oauth2: false,
}
.save_to_transports_table(&t, &EnteredLoginParam::default())
.await?;
let loaded = ConfiguredLoginParam::load(&t).await?.unwrap();
assert_eq!(loaded.provider, Some(*provider));
assert_eq!(loaded.imap.is_empty(), false);
assert_eq!(loaded.smtp.is_empty(), false);
assert_eq!(t.get_configured_provider().await?, Some(*provider));
Ok(())
}
}
+3
View File
@@ -33,6 +33,7 @@ use crate::reaction::get_msg_reactions;
use crate::sql;
use crate::summary::Summary;
use crate::sync::SyncData;
use crate::tools::create_outgoing_rfc724_mid;
use crate::tools::{
buf_compress, buf_decompress, get_filebytes, get_filemeta, gm2local_offset, read_file,
sanitize_filename, time, timestamp_to_str,
@@ -470,6 +471,7 @@ impl Message {
pub fn new(viewtype: Viewtype) -> Self {
Message {
viewtype,
rfc724_mid: create_outgoing_rfc724_mid(),
..Default::default()
}
}
@@ -479,6 +481,7 @@ impl Message {
Message {
viewtype: Viewtype::Text,
text,
rfc724_mid: create_outgoing_rfc724_mid(),
..Default::default()
}
}
+6 -6
View File
@@ -135,7 +135,7 @@ async fn test_get_width_height() {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_quote() {
async fn test_quote_basic() {
let d = TestContext::new().await;
let ctx = &d.ctx;
@@ -144,13 +144,10 @@ async fn test_quote() {
.unwrap();
let chat = d.create_chat_with_contact("", "dest@example.com").await;
let mut msg = Message::new_text("Quoted message".to_string());
// Send message, so it gets a Message-Id.
assert!(msg.rfc724_mid.is_empty());
let msg_id = chat::send_msg(ctx, chat.id, &mut msg).await.unwrap();
let msg = Message::load_from_db(ctx, msg_id).await.unwrap();
// Message has to be sent such that it gets saved to db.
chat::send_msg(ctx, chat.id, &mut msg).await.unwrap();
assert!(!msg.rfc724_mid.is_empty());
let mut msg2 = Message::new(Viewtype::Text);
@@ -358,6 +355,7 @@ async fn test_markseen_msgs() -> Result<()> {
let sent1 = alice.send_msg(alice_chat.id, &mut msg).await;
let msg1 = bob.recv_msg(&sent1).await;
let bob_chat_id = msg1.chat_id;
let mut msg = Message::new_text("this is the text!".to_string());
let sent2 = alice.send_msg(alice_chat.id, &mut msg).await;
let msg2 = bob.recv_msg(&sent2).await;
assert_eq!(msg1.chat_id, msg2.chat_id);
@@ -380,9 +378,11 @@ async fn test_markseen_msgs() -> Result<()> {
// bob sends to alice,
// alice knows bob and messages appear in normal chat
let mut msg = Message::new_text("this is the text!".to_string());
let msg1 = alice
.recv_msg(&bob.send_msg(bob_chat_id, &mut msg).await)
.await;
let mut msg = Message::new_text("this is the text!".to_string());
let msg2 = alice
.recv_msg(&bob.send_msg(bob_chat_id, &mut msg).await)
.await;
+74 -42
View File
@@ -13,6 +13,7 @@ use mail_builder::headers::HeaderType;
use mail_builder::mime::MimePart;
use tokio::fs;
use crate::aheader::{Aheader, EncryptPreference};
use crate::blob::BlobObject;
use crate::chat::{self, Chat};
use crate::config::Config;
@@ -22,6 +23,7 @@ use crate::contact::{Contact, ContactId, Origin};
use crate::context::Context;
use crate::e2ee::EncryptHelper;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::key::DcKey;
use crate::location;
use crate::message::{self, Message, MsgId, Viewtype};
use crate::mimeparser::{is_hidden, SystemMessage};
@@ -42,6 +44,7 @@ use crate::webxdc::StatusUpdateSerial;
pub const RECOMMENDED_FILE_SIZE: u64 = 24 * 1024 * 1024 / 4 * 3;
#[derive(Debug, Clone)]
#[expect(clippy::large_enum_variant)]
pub enum Loaded {
Message {
chat: Chat,
@@ -95,7 +98,7 @@ pub struct MimeFactory {
/// Vector of pairs of past group member names and addresses.
past_members: Vec<(String, String)>,
/// Timestamps of the members in the same order as in the `recipients`
/// Timestamps of the members in the same order as in the `to`
/// followed by `past_members`.
///
/// If this is not empty, its length
@@ -131,7 +134,6 @@ pub struct RenderedEmail {
pub message: String,
// pub envelope: Envelope,
pub is_encrypted: bool,
pub is_gossiped: bool,
pub last_added_location_id: Option<u32>,
/// A comma-separated string of sync-IDs that are used by the rendered email and must be deleted
@@ -433,35 +435,6 @@ impl MimeFactory {
}
}
async fn should_do_gossip(&self, context: &Context, multiple_recipients: bool) -> Result<bool> {
match &self.loaded {
Loaded::Message { chat, msg } => {
let cmd = msg.param.get_cmd();
if cmd == SystemMessage::MemberAddedToGroup
|| cmd == SystemMessage::SecurejoinMessage
{
Ok(true)
} else if multiple_recipients {
// beside key- and member-changes, force a periodic re-gossip.
let gossiped_timestamp = chat.id.get_gossiped_timestamp(context).await?;
let gossip_period = context.get_config_i64(Config::GossipPeriod).await?;
// `gossip_period == 0` is a special case for testing,
// enabling gossip in every message.
// Otherwise "smeared timestamps" may result in the condition
// to fail even if the clock is monotonic.
if gossip_period == 0 || time() >= gossiped_timestamp + gossip_period {
Ok(true)
} else {
Ok(false)
}
} else {
Ok(false)
}
}
Loaded::Mdn { .. } => Ok(false),
}
}
fn should_attach_profile_data(msg: &Message) -> bool {
msg.param.get_cmd() != SystemMessage::SecurejoinMessage || {
let step = msg.param.get(Param::Arg).unwrap_or_default();
@@ -781,8 +754,6 @@ impl MimeFactory {
}
}
let mut is_gossiped = false;
let peerstates = self.peerstates_for_recipients(context).await?;
let is_encrypted = !self.should_force_plaintext()
&& (e2ee_guaranteed || encrypt_helper.should_encrypt(context, &peerstates).await?);
@@ -950,16 +921,78 @@ impl MimeFactory {
// Add gossip headers in chats with multiple recipients
let multiple_recipients =
peerstates.len() > 1 || context.get_config_bool(Config::BccSelf).await?;
if self.should_do_gossip(context, multiple_recipients).await? {
for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) {
if let Some(header) = peerstate.render_gossip_header(verified) {
message = message.header(
"Autocrypt-Gossip",
mail_builder::headers::raw::Raw::new(header),
);
is_gossiped = true;
let gossip_period = context.get_config_i64(Config::GossipPeriod).await?;
let now = time();
match &self.loaded {
Loaded::Message { chat, msg } => {
if chat.typ != Chattype::Broadcast {
for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) {
let Some(key) = peerstate.peek_key(verified) else {
continue;
};
let fingerprint = key.dc_fingerprint().hex();
let cmd = msg.param.get_cmd();
let should_do_gossip = cmd == SystemMessage::MemberAddedToGroup
|| cmd == SystemMessage::SecurejoinMessage
|| multiple_recipients && {
let gossiped_timestamp: Option<i64> = context
.sql
.query_get_value(
"SELECT timestamp
FROM gossip_timestamp
WHERE chat_id=? AND fingerprint=?",
(chat.id, &fingerprint),
)
.await?;
// `gossip_period == 0` is a special case for testing,
// enabling gossip in every message.
//
// If current time is in the past compared to
// `gossiped_timestamp`, we also gossip because
// either the `gossiped_timestamp` or clock is wrong.
gossip_period == 0
|| gossiped_timestamp
.is_none_or(|ts| now >= ts + gossip_period || now < ts)
};
if !should_do_gossip {
continue;
}
let header = Aheader::new(
peerstate.addr.clone(),
key.clone(),
// Autocrypt 1.1.0 specification says that
// `prefer-encrypt` attribute SHOULD NOT be included.
EncryptPreference::NoPreference,
)
.to_string();
message = message.header(
"Autocrypt-Gossip",
mail_builder::headers::raw::Raw::new(header),
);
context
.sql
.execute(
"INSERT INTO gossip_timestamp (chat_id, fingerprint, timestamp)
VALUES (?, ?, ?)
ON CONFLICT (chat_id, fingerprint)
DO UPDATE SET timestamp=excluded.timestamp",
(chat.id, &fingerprint, now),
)
.await?;
}
}
}
Loaded::Mdn { .. } => {
// Never gossip in MDNs.
}
}
// Set the appropriate Content-Type for the inner message.
@@ -1103,7 +1136,6 @@ impl MimeFactory {
message,
// envelope: Envelope::new,
is_encrypted,
is_gossiped,
last_added_location_id,
sync_ids_to_delete: self.sync_ids_to_delete,
rfc724_mid,
+21 -17
View File
@@ -340,11 +340,12 @@ async fn test_subject_in_group() -> Result<()> {
// 6. Test that in a group, replies also take the quoted message's subject, while non-replies use the group title as subject
let t = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let group_id = chat::create_group_chat(&t, chat::ProtectionStatus::Unprotected, "groupname") // TODO encodings, ä
.await
.unwrap();
let bob = Contact::create(&t, "", "bob@example.org").await?;
chat::add_contact_to_chat(&t, group_id, bob).await?;
let bob_contact_id = t.add_or_lookup_contact_id(&bob).await;
chat::add_contact_to_chat(&t, group_id, bob_contact_id).await?;
let subject = send_msg_get_subject(&t, group_id, None).await?;
assert_eq!(subject, "groupname");
@@ -727,6 +728,7 @@ async fn test_selfavatar_unencrypted_signed() {
.is_some());
// if another message is sent, that one must not contain the avatar
let mut msg = Message::new_text("this is the text!".to_string());
let sent_msg = t.send_msg(chat.id, &mut msg).await;
let mut payload = sent_msg.payload().splitn(4, "\r\n\r\n");
@@ -772,19 +774,24 @@ async fn test_selfavatar_unencrypted_signed() {
/// Test that removed member address does not go into the `To:` field.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_remove_member_bcc() -> Result<()> {
let mut tcm = TestContextManager::new();
// Alice creates a group with Bob and Claire and then removes Bob.
let alice = TestContext::new_alice().await;
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let charlie = &tcm.charlie().await;
let claire_addr = "claire@foo.de";
let bob_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
let claire_id = Contact::create(&alice, "Claire", claire_addr).await?;
let bob_id = alice.add_or_lookup_contact_id(bob).await;
let charlie_id = alice.add_or_lookup_contact_id(charlie).await;
let charlie_contact = Contact::get_by_id(alice, charlie_id).await?;
let charlie_addr = charlie_contact.get_addr();
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
add_contact_to_chat(&alice, alice_chat_id, claire_id).await?;
send_text_msg(&alice, alice_chat_id, "Creating a group".to_string()).await?;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foo").await?;
add_contact_to_chat(alice, alice_chat_id, bob_id).await?;
add_contact_to_chat(alice, alice_chat_id, charlie_id).await?;
send_text_msg(alice, alice_chat_id, "Creating a group".to_string()).await?;
remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?;
remove_contact_from_chat(alice, alice_chat_id, charlie_id).await?;
let remove = alice.pop_sent_msg().await;
let remove_payload = remove.payload();
let parsed = mailparse::parse_mail(remove_payload.as_bytes())?;
@@ -796,8 +803,8 @@ async fn test_remove_member_bcc() -> Result<()> {
for to_addr in to.iter() {
match to_addr {
mailparse::MailAddr::Single(ref info) => {
// Addresses should be of existing members (Alice and Bob) and not Claire.
assert_ne!(info.addr, claire_addr);
// Addresses should be of existing members (Alice and Bob) and not Charlie.
assert_ne!(info.addr, charlie_addr);
}
mailparse::MailAddr::Group(_) => {
panic!("Group addresses are not expected here");
@@ -887,10 +894,7 @@ async fn test_dont_remove_self() -> Result<()> {
let mime_message = MimeMessage::from_bytes(alice, sent.payload.as_bytes(), None)
.await
.unwrap();
assert_eq!(
mime_message.get_header(HeaderDef::ChatGroupPastMembers),
None
);
assert!(!mime_message.header_exists(HeaderDef::ChatGroupPastMembers));
assert_eq!(
mime_message.chat_group_member_timestamps().unwrap().len(),
1 // There is a timestamp for Bob, not for Alice
+60 -19
View File
@@ -57,6 +57,10 @@ pub(crate) struct MimeMessage {
/// Message headers.
headers: HashMap<String, String>,
#[cfg(test)]
/// Names of removed (ignored) headers. Used by `header_exists()` needed for tests.
headers_removed: HashSet<String>,
/// List of addresses from the `To` and `Cc` headers.
///
/// Addresses are normalized and lowercase.
@@ -236,6 +240,7 @@ impl MimeMessage {
let mut hop_info = parse_receive_headers(&mail.get_headers());
let mut headers = Default::default();
let mut headers_removed = HashSet::<String>::new();
let mut recipients = Default::default();
let mut past_members = Default::default();
let mut from = Default::default();
@@ -253,7 +258,12 @@ impl MimeMessage {
&mut chat_disposition_notification_to,
&mail.headers,
);
headers.retain(|k, _| !is_hidden(k));
headers.retain(|k, _| {
!is_hidden(k) || {
headers_removed.insert(k.clone());
false
}
});
// Parse hidden headers.
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
@@ -298,9 +308,11 @@ impl MimeMessage {
// Overwrite Message-ID with X-Microsoft-Original-Message-ID.
// However if we later find Message-ID in the protected part,
// it will overwrite both.
if let Some(microsoft_message_id) =
headers.remove(HeaderDef::XMicrosoftOriginalMessageId.get_headername())
{
if let Some(microsoft_message_id) = remove_header(
&mut headers,
HeaderDef::XMicrosoftOriginalMessageId.get_headername(),
&mut headers_removed,
) {
headers.insert(
HeaderDef::MessageId.get_headername().to_string(),
microsoft_message_id,
@@ -309,7 +321,7 @@ impl MimeMessage {
// Remove headers that are allowed _only_ in the encrypted+signed part. It's ok to leave
// them in signed-only emails, but has no value currently.
Self::remove_secured_headers(&mut headers);
Self::remove_secured_headers(&mut headers, &mut headers_removed);
let mut from = from.context("No from in message")?;
let private_keyring = load_self_secret_keyring(context).await?;
@@ -330,10 +342,10 @@ impl MimeMessage {
let mail_raw; // Memory location for a possible decrypted message.
let decrypted_msg; // Decrypted signed OpenPGP message.
let (mail, encrypted) =
let (mail, is_encrypted) =
match tokio::task::block_in_place(|| try_decrypt(&mail, &private_keyring)) {
Ok(Some(msg)) => {
mail_raw = msg.get_content()?.unwrap_or_default();
Ok(Some(mut msg)) => {
mail_raw = msg.as_data_vec().unwrap_or_default();
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
@@ -422,7 +434,7 @@ impl MimeMessage {
signatures.extend(signatures_detached);
content
});
if let (Ok(mail), true) = (mail, encrypted) {
if let (Ok(mail), true) = (mail, is_encrypted) {
if !signatures.is_empty() {
// Remove unsigned opportunistically protected headers from messages considered
// Autocrypt-encrypted / displayed with padlock.
@@ -442,7 +454,7 @@ impl MimeMessage {
HeaderDef::ChatEdit,
HeaderDef::ChatUserAvatar,
] {
headers.remove(h.get_headername());
remove_header(&mut headers, h.get_headername(), &mut headers_removed);
}
}
@@ -506,7 +518,7 @@ impl MimeMessage {
}
}
if signatures.is_empty() {
Self::remove_secured_headers(&mut headers);
Self::remove_secured_headers(&mut headers, &mut headers_removed);
// If it is not a read receipt, degrade encryption.
if let (Some(peerstate), Ok(mail)) = (&mut peerstate, mail) {
@@ -517,7 +529,7 @@ impl MimeMessage {
}
}
}
if !encrypted {
if !is_encrypted {
signatures.clear();
}
if let Some(peerstate) = &mut peerstate {
@@ -530,6 +542,9 @@ impl MimeMessage {
let mut parser = MimeMessage {
parts: Vec::new(),
headers,
#[cfg(test)]
headers_removed,
recipients,
past_members,
list_post,
@@ -931,6 +946,16 @@ impl MimeMessage {
.map(|s| s.as_str())
}
#[cfg(test)]
/// Returns whether the header exists in any part of the parsed message.
///
/// Use this to check for header absense. Header presense should be checked using
/// `get_header(...).is_some()` as it also checks that the header isn't ignored.
pub(crate) fn header_exists(&self, headerdef: HeaderDef) -> bool {
let hname = headerdef.get_headername();
self.headers.contains_key(hname) || self.headers_removed.contains(hname)
}
/// Returns `Chat-Group-ID` header value if it is a valid group ID.
pub fn get_chat_group_id(&self) -> Option<&str> {
self.get_header(HeaderDef::ChatGroupId)
@@ -1526,14 +1551,17 @@ impl MimeMessage {
.and_then(|msgid| parse_message_id(msgid).ok())
}
fn remove_secured_headers(headers: &mut HashMap<String, String>) {
headers.remove("secure-join-fingerprint");
headers.remove("secure-join-auth");
headers.remove("chat-verified");
headers.remove("autocrypt-gossip");
fn remove_secured_headers(
headers: &mut HashMap<String, String>,
removed: &mut HashSet<String>,
) {
remove_header(headers, "secure-join-fingerprint", removed);
remove_header(headers, "secure-join-auth", removed);
remove_header(headers, "chat-verified", removed);
remove_header(headers, "autocrypt-gossip", removed);
// Secure-Join is secured unless it is an initial "vc-request"/"vg-request".
if let Some(secure_join) = headers.remove("secure-join") {
if let Some(secure_join) = remove_header(headers, "secure-join", removed) {
if secure_join == "vc-request" || secure_join == "vg-request" {
headers.insert("secure-join".to_string(), secure_join);
}
@@ -1861,6 +1889,19 @@ impl MimeMessage {
}
}
fn remove_header(
headers: &mut HashMap<String, String>,
key: &str,
removed: &mut HashSet<String>,
) -> Option<String> {
if let Some((k, v)) = headers.remove_entry(key) {
removed.insert(k);
Some(v)
} else {
None
}
}
/// Parses `Autocrypt-Gossip` headers from the email and applies them to peerstates.
/// Params:
/// from: The address which sent the message currently being parsed
@@ -2339,7 +2380,7 @@ async fn handle_ndn(
let aggregated_error = message
.error
.as_ref()
.map(|err| format!("{}\n\n{}", err, err_msg));
.map(|err| format!("{err}\n\n{err_msg}"));
set_msg_failed(
context,
&mut message,
+12
View File
@@ -261,6 +261,18 @@ async fn fetch_url(context: &Context, original_url: &str) -> Result<Response> {
continue;
}
if !response.status().is_success() {
return Err(anyhow!(
"The server returned a non-successful response code: {}{}",
response.status().as_u16(),
response
.status()
.canonical_reason()
.map(|s| format!(" {s}"))
.unwrap_or("".to_string())
));
}
let content_type = response
.headers()
.get("content-type")
+1
View File
@@ -158,6 +158,7 @@ impl Socks5Config {
/// (except for iroh p2p connections)
/// will be sent.
#[derive(Debug, Clone, PartialEq, Eq)]
#[expect(clippy::large_enum_variant)]
pub enum ProxyConfig {
/// HTTP proxy.
Http(HttpConfig),
+173 -52
View File
@@ -23,60 +23,157 @@
//! (scoped per WebXDC app instance/message-id). The other peers can then join the gossip with `joinRealtimeChannel().setListener()`
//! and `joinRealtimeChannel().send()` just like the other peers.
use anyhow::{anyhow, bail, Context as _, Result};
use anyhow::{Context as _, Result, anyhow, bail};
use data_encoding::BASE32_NOPAD;
use futures_lite::StreamExt;
use iroh::{Endpoint, NodeAddr, NodeId, PublicKey, RelayMap, RelayMode, RelayUrl, SecretKey};
use iroh_gossip::net::{Event, Gossip, GossipEvent, JoinOptions, GOSSIP_ALPN};
use iroh::{Endpoint, NodeAddr, NodeId, PublicKey, RelayMode, RelayUrl, SecretKey};
use iroh_gossip::net::{Event, GOSSIP_ALPN, Gossip, GossipEvent, JoinOptions};
use iroh_gossip::proto::TopicId;
use parking_lot::Mutex;
use std::collections::{BTreeSet, HashMap};
use std::env;
use tokio::sync::{oneshot, RwLock};
use tokio::sync::{RwLock, RwLockReadGuard, oneshot};
use tokio::task::JoinHandle;
use url::Url;
use crate::EventType;
use crate::chat::send_msg;
use crate::config::Config;
use crate::context::Context;
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::EventType;
/// The length of an ed25519 `PublicKey`, in bytes.
const PUBLIC_KEY_LENGTH: usize = 32;
const PUBLIC_KEY_STUB: &[u8] = "static_string".as_bytes();
/// Store iroh peer channels for the context.
#[derive(Debug)]
#[derive(Debug, Default)]
pub struct Iroh {
/// iroh router needed for iroh peer channels.
pub(crate) router: iroh::protocol::Router,
/// [Gossip] needed for iroh peer channels.
pub(crate) gossip: Gossip,
inner: RwLock<Option<EndpointState>>,
/// Sequence numbers for gossip channels.
pub(crate) sequence_numbers: Mutex<HashMap<TopicId, i32>>,
/// Topics for which an advertisement has already been sent.
pub(crate) iroh_channels: RwLock<HashMap<TopicId, ChannelState>>,
/// Currently used Iroh public key.
///
/// This is attached to every message to work around `iroh_gossip` deduplication.
pub(crate) public_key: PublicKey,
}
/// Iroh endpoint and gossip protocol state.
#[derive(Debug)]
struct EndpointState {
/// Router that runs the iroh Endpoint and hosts the gossip protocol.
router: iroh::protocol::Router,
/// The gossip protocol state and tasks.
gossip: Gossip,
/// Subscribed gossip topics.
channels: HashMap<TopicId, ChannelState>,
/// The public key of the iroh Endpoint.
public_key: PublicKey,
}
impl EndpointState {
fn close(self) {
// Spawn into a separate task, because iroh waits for the UDP sockets to be idle,
// which can take some time.
tokio::spawn(async move {
// We do not log the error because we do not want the task to hold a reference
// to the Context.
tokio::time::timeout(std::time::Duration::from_secs(60), self.router.shutdown())
.await
.ok();
});
}
}
// /// Store iroh peer channels for the context.
// #[derive(Debug)]
// pub struct Iroh {
// /// iroh router needed for iroh peer channels.
// router: iroh::protocol::Router,
// /// [Gossip] needed for iroh peer channels.
// gossip: Gossip,
// /// Sequence numbers for gossip channels.
// sequence_numbers: Mutex<HashMap<TopicId, i32>>,
// /// Topics for which an advertisement has already been sent.
// iroh_channels: RwLock<HashMap<TopicId, ChannelState>>,
// /// Currently used Iroh public key.
// ///
// /// This is attached to every message to work around `iroh_gossip` deduplication.
// public_key: PublicKey,
// }
impl Iroh {
/// Notify the endpoint that the network has changed.
pub(crate) async fn network_change(&self) {
self.router.endpoint().network_change().await
/// Creates an iroh endpoint and gossip if it does not yet exists.
async fn init(&self, ctx: &Context) -> Result<()> {
if self.inner.read().await.is_some() {
return Ok(());
}
info!(ctx, "Initializing peer channels.");
let mut inner = self.inner.write().await;
let secret_key = SecretKey::generate(rand::rngs::OsRng);
let public_key = secret_key.public();
let relay_mode = if let Some(relay_url) = ctx
.metadata
.read()
.await
.as_ref()
.and_then(|conf| conf.iroh_relay.clone())
{
RelayMode::Custom(RelayUrl::from(relay_url).into())
} else {
// FIXME: this should be RelayMode::Disabled instead.
// Currently using default relays because otherwise Rust tests fail.
RelayMode::Default
};
let endpoint = Endpoint::builder()
.tls_x509() // For compatibility with iroh <0.34.0
.secret_key(secret_key)
.alpns(vec![GOSSIP_ALPN.to_vec()])
.relay_mode(relay_mode)
.bind()
.await?;
// create gossip
// Allow messages up to 128 KB in size.
// We set the limit to 128 KiB to account for internal overhead,
// but only guarantee 128 KB of payload to WebXDC developers.
let gossip = Gossip::builder()
.max_message_size(128 * 1024)
.spawn(endpoint.clone())
.await?;
let router = iroh::protocol::Router::builder(endpoint)
.accept(GOSSIP_ALPN, gossip.clone())
.spawn();
*inner = Some(EndpointState {
router,
gossip,
channels: HashMap::new(),
public_key,
});
Ok(())
}
/// Closes the QUIC endpoint.
pub(crate) async fn close(self) -> Result<()> {
self.router.shutdown().await.context("Closing iroh failed")
/// Notify the endpoint that the network has changed.
pub(crate) async fn network_change(&self) {
let iroh = self.inner.read().await;
if let Some(inner) = iroh.as_ref() {
inner.router.endpoint().network_change().await;
}
}
/// Closes the iroh endpoint and router.
pub(crate) async fn close(&self) {
if let Some(iroh) = self.inner.write().await.take() {
iroh.close();
}
}
/// Join a topic and create the subscriber loop for it.
@@ -97,9 +194,8 @@ impl Iroh {
// no other thread can create a second gossip subscription
// after we check that it does not exist and before we create a new one.
// Otherwise we would receive every message twice or more times.
let mut iroh_channels = self.iroh_channels.write().await;
if iroh_channels.contains_key(&topic) {
let inner = self.inner.read().await;
if self.inner.read().await.channels.contains_key(&topic) {
return Ok(None);
}
@@ -245,7 +341,7 @@ impl Context {
.as_ref()
.and_then(|conf| conf.iroh_relay.clone())
{
RelayMode::Custom(RelayMap::from_url(RelayUrl::from(relay_url)))
RelayMode::Custom(RelayUrl::from(relay_url).into())
} else {
// FIXME: this should be RelayMode::Disabled instead.
// Currently using default relays because otherwise Rust tests fail.
@@ -253,6 +349,7 @@ impl Context {
};
let endpoint = Endpoint::builder()
.tls_x509() // For compatibility with iroh <0.34.0
.secret_key(secret_key)
.alpns(vec![GOSSIP_ALPN.to_vec()])
.relay_mode(relay_mode)
@@ -271,8 +368,7 @@ impl Context {
let router = iroh::protocol::Router::builder(endpoint)
.accept(GOSSIP_ALPN, gossip.clone())
.spawn()
.await?;
.spawn();
Ok(Iroh {
router,
@@ -484,6 +580,21 @@ pub async fn leave_webxdc_realtime(ctx: &Context, msg_id: MsgId) -> Result<()> {
iroh.leave_realtime(topic).await?;
info!(ctx, "IROH_REALTIME: Left gossip for message {msg_id}");
let can_close = iroh.iroh_channels.read().await.is_empty();
if can_close {
drop(iroh);
// Shut down iroh. Currently iroh does a lot of background work, which will improve
// post iroh 1.0, so shut it down when not needed.
info!(ctx, "XXXX can close");
if let Some(iroh) = ctx.iroh.write().await.take() {
info!(ctx, "Closing iroh Endpoint");
iroh.close()
.await
.map_err(|err| error!(ctx, "Error shutting down iroh: {err:#}"))
.ok();
}
}
Ok(())
}
@@ -553,10 +664,10 @@ async fn subscribe_loop(
mod tests {
use super::*;
use crate::{
EventType,
chat::send_msg,
message::{Message, Viewtype},
test_utils::TestContextManager,
EventType,
};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -728,15 +839,23 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_can_reconnect() {
tokio::time::timeout(tokio::time::Duration::from_secs(10), can_reconnect())
.await
.expect("timed out");
}
async fn can_reconnect() {
let mut tcm = TestContextManager::new();
let alice = &mut tcm.alice().await;
let bob = &mut tcm.bob().await;
assert!(alice
.get_config_bool(Config::WebxdcRealtimeEnabled)
.await
.unwrap());
// Alice sends webxdc to bob
assert!(
alice
.get_config_bool(Config::WebxdcRealtimeEnabled)
.await
.unwrap()
);
eprintln!("Alice sends webxdx to bob");
let alice_chat = alice.create_chat(bob).await;
let mut instance = Message::new(Viewtype::File);
instance
@@ -758,14 +877,14 @@ mod tests {
bob_webxdc.chat_id.accept(bob).await.unwrap();
// Alice advertises herself.
eprintln!("Alice advertises herself");
send_webxdc_realtime_advertisement(alice, alice_webxdc.id)
.await
.unwrap();
bob.recv_msg_trash(&alice.pop_sent_msg().await).await;
// Bob adds alice to gossip peers.
eprintln!("Bob adds alice to gossip peers.");
let members = get_iroh_gossip_peers(bob, bob_webxdc.id)
.await
.unwrap()
@@ -797,7 +916,7 @@ mod tests {
.await
.unwrap();
// Alice sends ephemeral message
eprintln!("Alice sends ephemeral message");
alice
.get_or_try_init_peer_channel()
.await
@@ -845,7 +964,7 @@ mod tests {
.lock()
.get(&bob_topic)
.copied();
// Check that sequence number is persisted when leaving the channel.
eprintln!("Check that sequence number is persisted when leaving the channel");
assert_eq!(bob_sequence_number, bob_sequence_number_after);
bob.get_or_try_init_peer_channel()
@@ -900,17 +1019,19 @@ mod tests {
.await
.unwrap()
.unwrap();
assert!(alice
.iroh
.read()
.await
.as_ref()
.unwrap()
.iroh_channels
.read()
.await
.get(&topic)
.is_none());
assert!(
alice
.iroh
.read()
.await
.as_ref()
.unwrap()
.iroh_channels
.read()
.await
.get(&topic)
.is_none()
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

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