Compare commits

..
Author SHA1 Message Date
link2xt e92127511a fix: allow to add transports even when I/O is running 2026-02-11 20:07:12 +00:00
link2xt c475882727 perf: use recv_direct() instead of recv() on the event channel
The difference between recv_direct()[1] and recv()[2]
is that recv() allocates memory for the future on the heap.
Using recv_direct() removes one pointer indirection.

[1] https://docs.rs/async-broadcast/0.7.2/async_broadcast/struct.Receiver.html#method.recv_direct
[2] https://docs.rs/async-broadcast/0.7.2/async_broadcast/struct.Receiver.html#method.recv
2026-02-10 01:32:40 +00:00
link2xt 166e259b18 chore: update fast-socks5 to version 1.0 2026-02-10 01:32:34 +00:00
link2xt cc38298163 refactor: enable clippy::manual_is_variant_and 2026-02-10 01:32:18 +00:00
link2xt 983f43c33c chore(release): prepare for 2.42.0 2026-02-10 00:37:13 +00:00
dependabot[bot] 5028842fd5 chore(cargo): bump quick-xml from 0.38.4 to 0.39.0
Bumps [quick-xml](https://github.com/tafia/quick-xml) from 0.38.4 to 0.39.0.
- [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.38.4...v0.39.0)

---
updated-dependencies:
- dependency-name: quick-xml
  dependency-version: 0.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-09 04:19:06 -03:00
link2xt e78b509d0a chore: update rPGP from 0.18.0 to 0.19.0 2026-02-08 21:09:38 +00:00
link2xt 583979c6fc fix: set mvbox_move to '0' explicitly for existing chatmail profiles
Otherwise if the user for some reason has no `mvbox_move` config set,
they get a device message about `mvbox_move` deprecation.
2026-02-08 01:58:23 +00:00
link2xt 5bfd8dd517 feat: do not scan not watched folders 2026-02-08 01:57:10 +00:00
link2xt 32b0ca81f8 test: remove test_dont_show_emails
Existing test relies on folder scanning.
We are going to remove the option to not show emails
(<https://github.com/chatmail/core/issues/7631>)
so the test will be removed eventually anyway.
2026-02-08 01:57:10 +00:00
d2weber 8dd7e5c5dd Fix typo in CHANGELOG for marknoticed_all_chats 2026-02-06 17:23:37 +01:00
link2xt 5bb0b86f6a chore(release): prepare for 2.41.0 2026-02-06 00:39:03 +00:00
B. Petersen ed2b0e8f03 feat: use different strings for audio and video calls 2026-02-05 21:54:52 +01:00
B. Petersen 8152ff518e fix: make use of call stock strings 2026-02-05 21:54:52 +01:00
dependabot[bot] cbcfb7087e chore(cargo): bump time from 0.3.37 to 0.3.47
Bumps [time](https://github.com/time-rs/time) from 0.3.37 to 0.3.47.
- [Release notes](https://github.com/time-rs/time/releases)
- [Changelog](https://github.com/time-rs/time/blob/main/CHANGELOG.md)
- [Commits](https://github.com/time-rs/time/compare/v0.3.37...v0.3.47)

---
updated-dependencies:
- dependency-name: time
  dependency-version: 0.3.47
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-05 19:14:15 +00:00
link2xt 396104af47 feat: do not require ShowEmails to be set to All for adding second relay 2026-02-05 19:12:08 +00:00
iequidoo 69f6727751 fix: Don't set download state to Failure if message is available on another Session's transport (#7684) 2026-02-05 18:58:52 +00:00
link2xt b72a677f4c chore(release): prepare for 2.40.0 2026-02-04 21:46:02 +00:00
link2xt 00e78eecf6 feat: add device message about legacy mvbox_move 2026-02-04 21:51:56 +01:00
link2xt 8b0621b724 test: set mvbox_move to 0 for test rust accounts 2026-02-04 21:51:56 +01:00
Casper Zandbergen 63bf4c4f33 feat: allow clients to specify whether a call has video initially or not (#7740) 2026-02-04 16:49:32 +00:00
iequidooandHocuri d6bce56d18 fix: Cross-account forwarding of a message which has_html() (#7791)
This includes forwarding of long messages. Also this fixes sending, but more likely resending of
forwarded messages for which the original message was deleted, because now we save HTML to the db
immediately when creating a forwarded message.

Co-authored-by: Hocuri <hocuri@gmx.de>
2026-02-04 11:41:27 -03:00
iequidoo c8dec0dcdd feat: Don't call BlobObject::create_and_deduplicate() when forwarding message to the same account
It has a really complex logic, so it's better to avoid calling it if possible than think which side
effects and performance penalties it has. It was never called here before adding forwarding messages
across contexts (accounts).
2026-02-04 11:41:27 -03:00
dependabot[bot] 509644ea5f chore(cargo): bump tracing-subscriber from 0.3.20 to 0.3.22
Bumps [tracing-subscriber](https://github.com/tokio-rs/tracing) from 0.3.20 to 0.3.22.
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.20...tracing-subscriber-0.3.22)

---
updated-dependencies:
- dependency-name: tracing-subscriber
  dependency-version: 0.3.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-04 03:22:29 +00:00
dependabot[bot] 3e95239e71 chore(cargo): bump rustls-pki-types from 1.13.2 to 1.14.0
Bumps [rustls-pki-types](https://github.com/rustls/pki-types) from 1.13.2 to 1.14.0.
- [Release notes](https://github.com/rustls/pki-types/releases)
- [Commits](https://github.com/rustls/pki-types/compare/v/1.13.2...v/1.14.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-04 01:38:52 +00:00
dependabot[bot] 74d4b823d2 chore(cargo): bump uuid from 1.19.0 to 1.20.0
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.19.0 to 1.20.0.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.19.0...v1.20.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-04 01:25:40 +00:00
dependabot[bot] 1bcfb90b90 chore(cargo): bump serde_json from 1.0.148 to 1.0.149
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.148 to 1.0.149.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.148...v1.0.149)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-version: 1.0.149
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 21:35:20 +00:00
dependabot[bot] 411ee511ed chore(cargo): bump toml from 0.9.10+spec-1.1.0 to 0.9.11+spec-1.1.0
Bumps [toml](https://github.com/toml-rs/toml) from 0.9.10+spec-1.1.0 to 0.9.11+spec-1.1.0.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.9.10...toml-v0.9.11)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 0.9.11+spec-1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 21:35:01 +00:00
dependabot[bot] e5a30c341c chore(cargo): bump tokio-stream from 0.1.17 to 0.1.18
Bumps [tokio-stream](https://github.com/tokio-rs/tokio) from 0.1.17 to 0.1.18.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-stream-0.1.17...tokio-stream-0.1.18)

---
updated-dependencies:
- dependency-name: tokio-stream
  dependency-version: 0.1.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 21:34:43 +00:00
link2xt 3d409c37a1 chore: remove RUSTSEC-2026-0002 exception from deny.toml
It is an "unsound" advisory for a transitive dependency
and cargo-deny does not report them by default
since cargo-deny 0.19.0.
2026-02-03 21:25:41 +00:00
dependabot[bot] b46c86c9b7 chore(deps): bump EmbarkStudios/cargo-deny-action from 2.0.14 to 2.0.15
Bumps [EmbarkStudios/cargo-deny-action](https://github.com/embarkstudios/cargo-deny-action) from 2.0.14 to 2.0.15.
- [Release notes](https://github.com/embarkstudios/cargo-deny-action/releases)
- [Commits](https://github.com/embarkstudios/cargo-deny-action/compare/76cd80eb775d7bbbd2d80292136d74d39e1b4918...3fd3802e88374d3fe9159b834c7714ec57d6c979)

---
updated-dependencies:
- dependency-name: EmbarkStudios/cargo-deny-action
  dependency-version: 2.0.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 21:25:41 +00:00
dependabot[bot] e5e268f503 chore(cargo): bump thiserror from 2.0.17 to 2.0.18
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 2.0.17 to 2.0.18.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/2.0.17...2.0.18)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-version: 2.0.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 20:54:49 +00:00
link2xt 633536bb13 fix: remove Config::DeleteToTrash and Config::ConfiguredTrashFolder
`delete_to_trash` is an option that was added for Gmail
as Gmail archives the messages by default
when they are deleted over IMAP:
<https://github.com/chatmail/core/issues/3957>
(implemented in <https://github.com/chatmail/core/pull/3972>).

Closes <https://github.com/chatmail/core/issues/6444>.
2026-02-03 18:31:55 +00:00
link2xt 94ee485155 chore: update provider database 2026-02-03 18:31:55 +00:00
link2xt ec0dc8bcad refactor: mark ProviderOptions as non_exhaustive
This prevents triggering clippy lint `needless_update`.
2026-02-03 18:31:55 +00:00
dependabot[bot] 49296e3014 chore(cargo): bump colorutils-rs from 0.7.5 to 0.7.6
Bumps [colorutils-rs](https://github.com/awxkee/colorutils-rs) from 0.7.5 to 0.7.6.
- [Release notes](https://github.com/awxkee/colorutils-rs/releases)
- [Commits](https://github.com/awxkee/colorutils-rs/compare/0.7.5...0.7.6)

---
updated-dependencies:
- dependency-name: colorutils-rs
  dependency-version: 0.7.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 18:17:53 +00:00
dependabot[bot] 2b93e856e4 chore(cargo): bump data-encoding from 2.9.0 to 2.10.0
Bumps [data-encoding](https://github.com/ia0/data-encoding) from 2.9.0 to 2.10.0.
- [Commits](https://github.com/ia0/data-encoding/compare/v2.9.0...v2.10.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 17:50:42 +00:00
dependabot[bot] c5be7df1d7 chore(cargo): bump chrono from 0.4.42 to 0.4.43
Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.42 to 0.4.43.
- [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.42...v0.4.43)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 17:29:05 +00:00
dependabot[bot] 6b74cb6539 chore(cargo): bump human-panic from 2.0.4 to 2.0.6
Bumps [human-panic](https://github.com/rust-cli/human-panic) from 2.0.4 to 2.0.6.
- [Changelog](https://github.com/rust-cli/human-panic/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-cli/human-panic/compare/v2.0.4...v2.0.6)

---
updated-dependencies:
- dependency-name: human-panic
  dependency-version: 2.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 17:28:50 +00:00
dependabot[bot] de2ac8cca2 chore(cargo): bump syn from 2.0.111 to 2.0.114
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.111 to 2.0.114.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.111...2.0.114)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 17:28:30 +00:00
dependabot[bot] 085fcd2751 chore(cargo): bump quote from 1.0.42 to 1.0.44
Bumps [quote](https://github.com/dtolnay/quote) from 1.0.42 to 1.0.44.
- [Release notes](https://github.com/dtolnay/quote/releases)
- [Commits](https://github.com/dtolnay/quote/compare/1.0.42...1.0.44)

---
updated-dependencies:
- dependency-name: quote
  dependency-version: 1.0.44
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 17:28:12 +00:00
dependabot[bot] 83f30e4a54 chore(cargo): bump libc from 0.2.178 to 0.2.180
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.178 to 0.2.180.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.180/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.178...0.2.180)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 17:27:56 +00:00
dependabot[bot] e79b4baa09 chore(cargo): bump tokio-util from 0.7.17 to 0.7.18
Bumps [tokio-util](https://github.com/tokio-rs/tokio) from 0.7.17 to 0.7.18.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-util-0.7.17...tokio-util-0.7.18)

---
updated-dependencies:
- dependency-name: tokio-util
  dependency-version: 0.7.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 17:27:43 +00:00
dependabot[bot] 1e0c0d8efa chore(cargo): bump tokio from 1.48.0 to 1.49.0
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.48.0 to 1.49.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.48.0...tokio-1.49.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 17:27:10 +00:00
link2xt 378fb09c80 ci: make scripts/deny.sh test the locked version of dependencies 2026-02-03 17:25:42 +00:00
link2xt ff2fbebff0 chore(cargo): update bytes from 1.11.0 to 1.11.1
Fixes <https://rustsec.org/advisories/RUSTSEC-2026-0007>
2026-02-03 17:25:42 +00:00
62 changed files with 795 additions and 1133 deletions
+2 -2
View File
@@ -59,9 +59,9 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: EmbarkStudios/cargo-deny-action@76cd80eb775d7bbbd2d80292136d74d39e1b4918
- uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979
with:
arguments: --all-features --workspace
arguments: --workspace --all-features --locked
command: check
command-arguments: "-Dwarnings"
+120 -1
View File
@@ -1,5 +1,120 @@
# Changelog
## [2.42.0] - 2026-02-10
### Fixes
- Set `mvbox_move` to '0' explicitly for existing chatmail profiles.
It's needed to prevent device message about deprecated `mvbox_move` option from appearing in chatmail profiles.
### Features / Changes
- Do not scan not watched folders.
### Miscellaneous Tasks
- Update rPGP from 0.18.0 to 0.19.0.
- cargo: Bump quick-xml from 0.38.4 to 0.39.0.
### Tests
- Remove test_dont_show_emails.
### Other
- Fix typo in CHANGELOG for marknoticed_all_chats.
## [2.41.0] - 2026-02-06
### Features / Changes
- Do not require `ShowEmails` to be set to `All` for adding second relay.
- Use different strings for audio and video calls.
### Fixes
- Don't set download state to Failure if message is available on another Session's transport ([#7684](https://github.com/chatmail/core/pull/7684)).
- Make use of call stock strings.
### Miscellaneous Tasks
- cargo: Bump `time` from 0.3.37 to 0.3.47.
## [2.40.0] - 2026-02-04
### Features / Changes
- Receive_imf: Log reasoning for chat assignment.
- Use more fitting encryption info message.
- Send Intended Recipient Fingerprint subpackets.
- Trash messages with intended recipient fingerprints, but w/o our one included.
- Do not collect email addresses from messages after configuration.
- Add device message about legacy `mvbox_move`.
- Never create IMAP folders.
- Make summary for pre-messages look like summary for fully downloaded messages ([#7775](https://github.com/chatmail/core/pull/7775)).
- Don't call `BlobObject::create_and_deduplicate()` when forwarding message to the same account.
- Allow clients to specify whether a call has video initially or not ([#7740](https://github.com/chatmail/core/pull/7740)).
- Do not load more than one own key from the keychain.
### Fixes
- Cross-account forwarding of a message which `has_html()` ([#7791](https://github.com/chatmail/core/pull/7791)).
- Make self-contact a key-contact even if key isn't generated yet.
- `apply_group_changes()`: Check whether From is key-contact.
- Don't add SELF to unencrypted chat created from encrypted message ([#7661](https://github.com/chatmail/core/pull/7661)).
- Don't upscale images and test that image resolution isn't changed unnecessarily ([#7769](https://github.com/chatmail/core/pull/7769)).
- Restart i/o when there are new transports in a sync message ([#7640](https://github.com/chatmail/core/pull/7640)).
- `add_or_lookup_key_contacts*()`: Advance fingerprint_iter on invalid address.
- `receive_imf`: Look up key contact by intended recipient fingerprint ([#7661](https://github.com/chatmail/core/pull/7661)).
- Remove `Config::DeleteToTrash` and `Config::ConfiguredTrashFolder`.
### API-Changes
- jsonrpc(python): Process events forever by default.
### CI
- Make scripts/deny.sh test the locked version of dependencies.
### Refactor
- Remove unneeded dbg! statements ([#7776](https://github.com/chatmail/core/pull/7776)).
- Remove unused Context.is_inbox().
- Rename lookup_key_contacts_by_address_list() to lookup_key_contacts_fallback_to_chat().
- Mark `ProviderOptions` as `non_exhaustive`.
### Miscellaneous Tasks
- Update provider database.
- cargo: Update `bytes` from 1.11.0 to 1.11.1.
- cargo: Bump tokio from 1.48.0 to 1.49.0.
- cargo: Bump tokio-util from 0.7.17 to 0.7.18.
- cargo: Bump libc from 0.2.178 to 0.2.180.
- cargo: Bump quote from 1.0.42 to 1.0.44.
- cargo: Bump syn from 2.0.111 to 2.0.114.
- cargo: Bump human-panic from 2.0.4 to 2.0.6.
- cargo: Bump chrono from 0.4.42 to 0.4.43.
- cargo: Bump data-encoding from 2.9.0 to 2.10.0.
- cargo: Bump colorutils-rs from 0.7.5 to 0.7.6.
- Update provider database.
- cargo: Bump thiserror from 2.0.17 to 2.0.18.
- deps: Bump EmbarkStudios/cargo-deny-action from 2.0.14 to 2.0.15.
- Remove RUSTSEC-2026-0002 exception from deny.toml.
- cargo: Bump tokio-stream from 0.1.17 to 0.1.18.
- cargo: Bump toml from 0.9.10+spec-1.1.0 to 0.9.11+spec-1.1.0.
- cargo: Bump serde_json from 1.0.148 to 1.0.149.
- cargo: Bump uuid from 1.19.0 to 1.20.0.
- cargo: Bump rustls-pki-types from 1.13.2 to 1.14.0.
- cargo: Bump tracing-subscriber from 0.3.20 to 0.3.22.
### Tests
- 2nd device receives message via new primary transport.
- Make `test_dont_move_sync_msgs` less flaky.
- Encrypted incoming message goes to encrypted 1:1 chat even if references messages in ad-hoc group.
- Message in blocked chat arrives as InSeen.
- Set `mvbox_move` to 0 for test rust accounts.
## [2.39.0] - 2026-01-23
### CI
@@ -28,7 +143,7 @@
- [**breaking**] Jsonrpc: remove `contacts` from `FullChat`. To migrate load contacts on demand via `get_contacts_by_ids` using `FullChat.contactIds` ([#7282](https://github.com/chatmail/core/pull/7282)).
- jsonrpc: Add run_until parameter for bots ([#7688](https://github.com/chatmail/core/pull/7688)).
- rust, jsonrpc: Add `get_message_read_receipt_count` method ([#7732](https://github.com/chatmail/core/pull/7732)).
- rust and jsonrpc: Marknoticed_all_chats method to mark all chats as notices, including muted ones. ([#7709](https://github.com/chatmail/core/pull/7709)).
- rust and jsonrpc: Marknoticed_all_chats method to mark all chats as noticed, including muted ones. ([#7709](https://github.com/chatmail/core/pull/7709)).
- Public re-export of Connectivity ([#7737](https://github.com/chatmail/core/pull/7737)).
### Documentation
@@ -7619,3 +7734,7 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[2.36.0]: https://github.com/chatmail/core/compare/v2.35.0..v2.36.0
[2.37.0]: https://github.com/chatmail/core/compare/v2.36.0..v2.37.0
[2.38.0]: https://github.com/chatmail/core/compare/v2.37.0..v2.38.0
[2.39.0]: https://github.com/chatmail/core/compare/v2.38.0..v2.39.0
[2.40.0]: https://github.com/chatmail/core/compare/v2.39.0..v2.40.0
[2.41.0]: https://github.com/chatmail/core/compare/v2.40.0..v2.41.0
[2.42.0]: https://github.com/chatmail/core/compare/v2.41.0..v2.42.0
Generated
+178 -156
View File
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "2.39.0"
version = "2.42.0"
edition = "2024"
license = "MPL-2.0"
rust-version = "1.88"
@@ -56,7 +56,7 @@ chrono = { workspace = true, features = ["alloc", "clock", "std"] }
colorutils-rs = { version = "0.7.5", default-features = false }
data-encoding = "2.9.0"
escaper = "0.1"
fast-socks5 = "0.10"
fast-socks5 = "1"
fd-lock = "4"
futures-lite = { workspace = true }
futures = { workspace = true }
@@ -78,10 +78,10 @@ num-derive = "0.4"
num-traits = { workspace = true }
parking_lot = "0.12.4"
percent-encoding = "2.3"
pgp = { version = "0.18.0", default-features = false }
pgp = { version = "0.19.0", default-features = false }
pin-project = "1"
qrcodegen = "1.7.0"
quick-xml = { version = "0.38", features = ["escape-html"] }
quick-xml = { version = "0.39", features = ["escape-html"] }
rand-old = { package = "rand", version = "0.8" }
rand = { workspace = true }
regex = { workspace = true }
@@ -182,7 +182,7 @@ harness = false
anyhow = "1"
async-channel = "2.5.0"
base64 = "0.22"
chrono = { version = "0.4.42", default-features = false }
chrono = { version = "0.4.43", default-features = false }
deltachat-contact-tools = { path = "deltachat-contact-tools" }
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
deltachat = { path = ".", default-features = false }
@@ -202,7 +202,7 @@ serde_json = "1"
tempfile = "3.24.0"
thiserror = "2"
tokio = "1"
tokio-util = "0.7.17"
tokio-util = "0.7.18"
tracing-subscriber = "0.3"
yerpc = "0.6.4"
+1 -1
View File
@@ -58,7 +58,7 @@ async fn create_context() -> Context {
.await
.unwrap();
let secret = key_from_asc(include_str!("../test-data/key/bob-secret.asc")).unwrap();
let public = secret.signed_public_key();
let public = secret.to_public_key();
let key_pair = KeyPair { public, secret };
store_self_keypair(&context, &key_pair)
.await
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "2.39.0"
version = "2.42.0"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"
+16 -7
View File
@@ -1242,9 +1242,12 @@ uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t c
* This needs to be a one-to-one chat.
* @param place_call_info any data that other devices receive
* in #DC_EVENT_INCOMING_CALL.
* @param has_video Whether the call has video initially.
* This allows the recipient's client to adjust incoming call UX.
* A call can be upgraded to include video later.
* @return ID of the system message announcing the call.
*/
uint32_t dc_place_outgoing_call (dc_context_t* context, uint32_t chat_id, const char* place_call_info);
uint32_t dc_place_outgoing_call (dc_context_t* context, uint32_t chat_id, const char* place_call_info, int has_video);
/**
@@ -7541,12 +7544,6 @@ void dc_event_unref(dc_event_t* event);
/// "❤️ Seems you're enjoying Delta Chat!"… (donation request device message)
#define DC_STR_DONATION_REQUEST 193
/// "Outgoing call"
#define DC_STR_OUTGOING_CALL 194
/// "Incoming call"
#define DC_STR_INCOMING_CALL 195
/// "Declined call"
#define DC_STR_DECLINED_CALL 196
@@ -7598,6 +7595,18 @@ void dc_event_unref(dc_event_t* event);
/// Used as the first info messages in newly created classic email threads.
#define DC_STR_CHAT_UNENCRYPTED_EXPLANATON 230
/// "Outgoing audio call"
#define DC_STR_OUTGOING_AUDIO_CALL 232
/// "Outgoing video call"
#define DC_STR_OUTGOING_VIDEO_CALL 233
/// "Incoming audio call"
#define DC_STR_INCOMING_AUDIO_CALL 234
/// "Incoming video call"
#define DC_STR_INCOMING_VIDEO_CALL 235
/**
* @}
*/
+2 -1
View File
@@ -1181,6 +1181,7 @@ pub unsafe extern "C" fn dc_place_outgoing_call(
context: *mut dc_context_t,
chat_id: u32,
place_call_info: *const libc::c_char,
has_video: bool,
) -> u32 {
if context.is_null() || chat_id == 0 {
eprintln!("ignoring careless call to dc_place_outgoing_call()");
@@ -1190,7 +1191,7 @@ pub unsafe extern "C" fn dc_place_outgoing_call(
let chat_id = ChatId::new(chat_id);
let place_call_info = to_string_lossy(place_call_info);
block_on(ctx.place_outgoing_call(chat_id, place_call_info))
block_on(ctx.place_outgoing_call(chat_id, place_call_info, has_video))
.context("Failed to place call")
.log_err(ctx)
.map(|msg_id| msg_id.to_u32())
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "2.39.0"
version = "2.42.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
license = "MPL-2.0"
+2 -1
View File
@@ -2167,10 +2167,11 @@ impl CommandApi {
account_id: u32,
chat_id: u32,
place_call_info: String,
has_video: bool,
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let msg_id = ctx
.place_outgoing_call(ChatId::new(chat_id), place_call_info)
.place_outgoing_call(ChatId::new(chat_id), place_call_info, has_video)
.await?;
Ok(msg_id.to_u32())
}
+3 -3
View File
@@ -1,6 +1,6 @@
use anyhow::{Context as _, Result};
use deltachat::calls::{call_state, sdp_has_video, CallState};
use deltachat::calls::{call_state, CallState};
use deltachat::context::Context;
use deltachat::message::MsgId;
use serde::Serialize;
@@ -15,7 +15,7 @@ pub struct JsonrpcCallInfo {
/// even if incoming call event was missed.
pub sdp_offer: String,
/// True if SDP offer has a video.
/// True if the call is started as a video call.
pub has_video: bool,
/// Call state.
@@ -30,7 +30,7 @@ impl JsonrpcCallInfo {
format!("Attempting to get call state of non-call message {msg_id}")
})?;
let sdp_offer = call_info.place_call_info.clone();
let has_video = sdp_has_video(&sdp_offer).unwrap_or_default();
let has_video = call_info.has_video_initially();
let state = JsonrpcCallState::from_msg_id(context, msg_id).await?;
Ok(JsonrpcCallInfo {
+1 -1
View File
@@ -54,5 +54,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "2.39.0"
"version": "2.42.0"
}
+3 -34
View File
@@ -40,51 +40,20 @@ export class BaseDeltaChat<
* and emitting the respective events on this class.
*/
startEventLoop: boolean,
options?: {
/**
* @see {@linkcode BaseDeltaChat.eventLoop}.
*
* Has no effect if {@linkcode startEventLoop} === false.
*/
eventLoopRequestPoolSize?: number;
},
) {
super();
this.rpc = new RawClient(this.transport);
if (startEventLoop) {
this.eventTask = this.eventLoop({
eventLoopRequestPoolSize: options?.eventLoopRequestPoolSize,
});
this.eventTask = this.eventLoop();
}
}
/**
* @see the constructor's `startEventLoop`
*/
async eventLoop(options?: {
/**
* How many {@linkcode RawClient.getNextEvent} to constantly keep open.
* Having a value > 1 improves performance
* when dealing with bursts of events.
*
* Must be >= 1.
*
* @default 20
*/
eventLoopRequestPoolSize?: number;
}): Promise<void> {
const promises: ReturnType<typeof this.rpc.getNextEvent>[] = [];
for (let i = 0; i < (options?.eventLoopRequestPoolSize ?? 20); i++) {
promises.push(this.rpc.getNextEvent());
}
const bufferLength = promises.length;
let currInd = 0;
async eventLoop(): Promise<void> {
while (true) {
const event = await promises[currInd];
promises[currInd] = this.rpc.getNextEvent();
currInd = (currInd + 1) % bufferLength;
const event = await this.rpc.getNextEvent();
//@ts-ignore
this.emit(event.event.kind, event.contextId, event.event);
this.emit("ALL", event.contextId, event.event);
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "2.39.0"
version = "2.42.0"
license = "MPL-2.0"
edition = "2021"
repository = "https://github.com/chatmail/core"
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "2.39.0"
version = "2.42.0"
license = "MPL-2.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
@@ -303,7 +303,7 @@ class Chat:
f.flush()
self._rpc.send_msg(self.account.id, self.id, {"viewtype": ViewType.VCARD, "file": f.name})
def place_outgoing_call(self, place_call_info: str) -> Message:
def place_outgoing_call(self, place_call_info: str, has_video_initially: bool) -> Message:
"""Starts an outgoing call."""
msg_id = self._rpc.place_outgoing_call(self.account.id, self.id, place_call_info)
msg_id = self._rpc.place_outgoing_call(self.account.id, self.id, place_call_info, has_video_initially)
return Message(self.account, msg_id)
+24 -32
View File
@@ -10,15 +10,15 @@ def test_calls(acfactory) -> None:
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
outgoing_call_message = alice_chat_bob.place_outgoing_call(place_call_info)
outgoing_call_message = alice_chat_bob.place_outgoing_call(place_call_info, has_video_initially=True)
assert outgoing_call_message.get_call_info().state.kind == "Alerting"
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
assert incoming_call_event.place_call_info == place_call_info
assert not incoming_call_event.has_video # Cannot be parsed as SDP, so false by default
assert incoming_call_event.has_video
incoming_call_message = Message(bob, incoming_call_event.msg_id)
assert incoming_call_message.get_call_info().state.kind == "Alerting"
assert not incoming_call_message.get_call_info().has_video
assert incoming_call_message.get_call_info().has_video
incoming_call_message.accept_incoming_call(accept_call_info)
assert incoming_call_message.get_call_info().sdp_offer == place_call_info
@@ -41,46 +41,38 @@ def test_video_call(acfactory) -> None:
#
# `s=` cannot be empty according to RFC 3264,
# so it is more clear as `s=-`.
place_call_info = """v=0\r
o=alice 2890844526 2890844526 IN IP6 2001:db8::3\r
s=-\r
c=IN IP6 2001:db8::3\r
t=0 0\r
a=group:BUNDLE foo bar\r
\r
m=audio 10000 RTP/AVP 0 8 97\r
b=AS:200\r
a=mid:foo\r
a=rtcp-mux\r
a=rtpmap:0 PCMU/8000\r
a=rtpmap:8 PCMA/8000\r
a=rtpmap:97 iLBC/8000\r
a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r
\r
m=video 10002 RTP/AVP 31 32\r
b=AS:1000\r
a=mid:bar\r
a=rtcp-mux\r
a=rtpmap:31 H261/90000\r
a=rtpmap:32 MPV/90000\r
a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r
"""
alice, bob = acfactory.get_online_accounts(2)
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.place_outgoing_call(place_call_info)
alice_chat_bob.place_outgoing_call("offer", has_video_initially=True)
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
assert incoming_call_event.place_call_info == place_call_info
assert incoming_call_event.place_call_info == "offer"
assert incoming_call_event.has_video
incoming_call_message = Message(bob, incoming_call_event.msg_id)
assert incoming_call_message.get_call_info().has_video
def test_audio_call(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.place_outgoing_call("offer", has_video_initially=False)
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
assert incoming_call_event.place_call_info == "offer"
assert not incoming_call_event.has_video
incoming_call_message = Message(bob, incoming_call_event.msg_id)
assert not incoming_call_message.get_call_info().has_video
def test_ice_servers(acfactory) -> None:
alice = acfactory.get_online_account()
@@ -92,7 +84,7 @@ def test_no_contact_request_call(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.place_outgoing_call("offer")
alice_chat_bob.place_outgoing_call("offer", has_video_initially=True)
alice_chat_bob.send_text("Hello!")
# Notification for "Hello!" message should arrive
@@ -119,7 +111,7 @@ def test_who_can_call_me_nobody(acfactory) -> None:
bob.create_chat(alice)
alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.place_outgoing_call("offer")
alice_chat_bob.place_outgoing_call("offer", has_video_initially=True)
alice_chat_bob.send_text("Hello!")
# Notification for "Hello!" message should arrive
@@ -144,7 +136,7 @@ def test_who_can_call_me_everybody(acfactory) -> None:
bob.set_config("who_can_call_me", "0")
alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.place_outgoing_call("offer")
alice_chat_bob.place_outgoing_call("offer", has_video_initially=True)
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
incoming_call_message = Message(bob, incoming_call_event.msg_id)
+2 -317
View File
@@ -26,70 +26,6 @@ def test_move_works(acfactory, direct_imap):
assert msg.text == "message1"
def test_move_avoids_loop(acfactory, direct_imap):
"""Test that the message is only moved from INBOX to DeltaChat.
This is to avoid busy loop if moved message reappears in the Inbox
or some scanned folder later.
For example, this happens on servers that alias `INBOX.DeltaChat` to `DeltaChat` folder,
so the message moved to `DeltaChat` appears as a new message in the `INBOX.DeltaChat` folder.
We do not want to move this message from `INBOX.DeltaChat` to `DeltaChat` again.
"""
ac1, ac2 = acfactory.get_online_accounts(2)
ac2_direct_imap = direct_imap(ac2)
ac2_direct_imap.create_folder("DeltaChat")
ac2.set_config("mvbox_move", "1")
ac2.set_config("delete_server_after", "0")
ac2.bring_online()
# Create INBOX.DeltaChat folder and make sure
# it is detected by full folder scan.
ac2_direct_imap = direct_imap(ac2)
ac2_direct_imap.create_folder("INBOX.DeltaChat")
ac2.stop_io()
ac2.start_io()
while True:
event = ac2.wait_for_event()
# Wait until the end of folder scan.
if event.kind == EventType.INFO and "Found folders:" in event.msg:
break
ac1_chat = acfactory.get_accepted_chat(ac1, ac2)
ac1_chat.send_text("Message 1")
# Message is moved to the DeltaChat folder and downloaded.
ac2_msg1 = ac2.wait_for_incoming_msg().get_snapshot()
assert ac2_msg1.text == "Message 1"
# Move the message to the INBOX.DeltaChat again.
# We assume that test server uses "." as the delimiter.
ac2_direct_imap.select_folder("DeltaChat")
ac2_direct_imap.conn.move(["*"], "INBOX.DeltaChat")
ac1_chat.send_text("Message 2")
ac2_msg2 = ac2.wait_for_incoming_msg().get_snapshot()
assert ac2_msg2.text == "Message 2"
# Stop and start I/O to trigger folder scan.
ac2.stop_io()
ac2.start_io()
while True:
event = ac2.wait_for_event()
# Wait until the end of folder scan.
if event.kind == EventType.INFO and "Found folders:" in event.msg:
break
# Check that Message 1 is still in the INBOX.DeltaChat folder
# and Message 2 is in the DeltaChat folder.
ac2_direct_imap.select_folder("INBOX")
assert len(ac2_direct_imap.get_all_messages()) == 0
ac2_direct_imap.select_folder("DeltaChat")
assert len(ac2_direct_imap.get_all_messages()) == 1
ac2_direct_imap.select_folder("INBOX.DeltaChat")
assert len(ac2_direct_imap.get_all_messages()) == 1
def test_reactions_for_a_reordering_move(acfactory, direct_imap):
"""When a batch of messages is moved from Inbox to DeltaChat folder with a single MOVE command,
their UIDs may be reordered (e.g. Gmail is known for that) which led to that messages were
@@ -136,146 +72,6 @@ def test_reactions_for_a_reordering_move(acfactory, direct_imap):
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
def test_dont_show_emails(acfactory, direct_imap, log):
"""Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them.
So: If it's outgoing AND there is no Received header, then ignore the email.
If the draft email is sent out and received later (i.e. it's in "Inbox"), it must be shown.
Also, test that unknown emails in the Spam folder are not shown."""
ac1 = acfactory.new_configured_account()
ac1.stop_io()
ac1.set_config("show_emails", "2")
ac1.create_contact("alice@example.org").create_chat()
ac1_direct_imap = direct_imap(ac1)
ac1_direct_imap.create_folder("Drafts")
ac1_direct_imap.create_folder("Spam")
ac1_direct_imap.create_folder("Junk")
# Learn UID validity for all folders.
ac1.set_config("scan_all_folders_debounce_secs", "0")
ac1.start_io()
ac1.wait_for_event(EventType.IMAP_INBOX_IDLE)
ac1.stop_io()
ac1_direct_imap.append(
"Drafts",
"""
From: ac1 <{}>
Subject: subj
To: alice@example.org
Message-ID: <aepiors@example.org>
Content-Type: text/plain; charset=utf-8
message in Drafts received later
""".format(
ac1.get_config("configured_addr"),
),
)
ac1_direct_imap.append(
"Spam",
"""
From: unknown.address@junk.org
Subject: subj
To: {}
Message-ID: <spam.message@junk.org>
Content-Type: text/plain; charset=utf-8
Unknown message in Spam
""".format(
ac1.get_config("configured_addr"),
),
)
ac1_direct_imap.append(
"Spam",
"""
From: unknown.address@junk.org, unkwnown.add@junk.org
Subject: subj
To: {}
Message-ID: <spam.message2@junk.org>
Content-Type: text/plain; charset=utf-8
Unknown & malformed message in Spam
""".format(
ac1.get_config("configured_addr"),
),
)
ac1_direct_imap.append(
"Spam",
"""
From: delta<address: inbox@nhroy.com>
Subject: subj
To: {}
Message-ID: <spam.message99@junk.org>
Content-Type: text/plain; charset=utf-8
Unknown & malformed message in Spam
""".format(
ac1.get_config("configured_addr"),
),
)
ac1_direct_imap.append(
"Spam",
"""
From: alice@example.org
Subject: subj
To: {}
Message-ID: <spam.message3@junk.org>
Content-Type: text/plain; charset=utf-8
Actually interesting message in Spam
""".format(
ac1.get_config("configured_addr"),
),
)
ac1_direct_imap.append(
"Junk",
"""
From: unknown.address@junk.org
Subject: subj
To: {}
Message-ID: <spam.message@junk.org>
Content-Type: text/plain; charset=utf-8
Unknown message in Junk
""".format(
ac1.get_config("configured_addr"),
),
)
ac1.set_config("scan_all_folders_debounce_secs", "0")
log.section("All prepared, now let DC find the message")
ac1.start_io()
# Wait until each folder was scanned, this is necessary for this test to test what it should test:
ac1.wait_for_event(EventType.IMAP_INBOX_IDLE)
fresh_msgs = list(ac1.get_fresh_messages())
msg = fresh_msgs[0].get_snapshot()
chat_msgs = msg.chat.get_messages()
assert len(chat_msgs) == 1
assert msg.text == "subj – Actually interesting message in Spam"
assert not any("unknown.address" in c.get_full_snapshot().name for c in ac1.get_chatlist())
ac1_direct_imap.select_folder("Spam")
assert ac1_direct_imap.get_uid_by_message_id("spam.message@junk.org")
ac1.stop_io()
log.section("'Send out' the draft by moving it to Inbox, and wait for DC to display it this time")
ac1_direct_imap.select_folder("Drafts")
uid = ac1_direct_imap.get_uid_by_message_id("aepiors@example.org")
ac1_direct_imap.conn.move(uid, "Inbox")
ac1.start_io()
event = ac1.wait_for_event(EventType.MSGS_CHANGED)
msg2 = Message(ac1, event.msg_id).get_snapshot()
assert msg2.text == "subj – message in Drafts received later"
assert len(msg.chat.get_messages()) == 2
def test_move_works_on_self_sent(acfactory, direct_imap):
ac1, ac2 = acfactory.get_online_accounts(2)
@@ -378,121 +174,12 @@ def test_markseen_message_and_mdn(acfactory, direct_imap, mvbox_move):
assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 1
def test_mvbox_and_trash(acfactory, direct_imap, log):
log.section("ac1: start with mvbox")
ac1 = acfactory.get_online_account()
ac1_direct_imap = direct_imap(ac1)
ac1_direct_imap.create_folder("DeltaChat")
ac1.set_config("mvbox_move", "1")
ac1.bring_online()
log.section("ac2: start without a mvbox")
ac2 = acfactory.get_online_account()
log.section("ac1: create trash")
ac1_direct_imap.create_folder("Trash")
ac1.set_config("scan_all_folders_debounce_secs", "0")
ac1.stop_io()
ac1.start_io()
log.section("ac1: send message and wait for ac2 to receive it")
acfactory.get_accepted_chat(ac1, ac2).send_text("message1")
assert ac2.wait_for_incoming_msg().get_snapshot().text == "message1"
assert ac1.get_config("configured_mvbox_folder") == "DeltaChat"
while ac1.get_config("configured_trash_folder") != "Trash":
ac1.wait_for_event(EventType.CONNECTIVITY_CHANGED)
@pytest.mark.parametrize(
("folder", "move", "expected_destination"),
[
(
"xyz",
False,
"xyz",
), # Test that emails aren't found in a random folder
(
"xyz",
True,
"xyz",
), # ...emails are found in a random folder and downloaded without moving
(
"Spam",
False,
"INBOX",
), # ...emails are moved from the spam folder to the Inbox
],
)
# Testrun.org does not support the CREATE-SPECIAL-USE capability, which means that we can't create a folder with
# the "\Junk" flag (see https://tools.ietf.org/html/rfc6154). So, we can't test spam folder detection by flag.
def test_scan_folders(acfactory, log, direct_imap, folder, move, expected_destination):
"""Delta Chat periodically scans all folders for new messages to make sure we don't miss any."""
variant = folder + "-" + str(move) + "-" + expected_destination
log.section("Testing variant " + variant)
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("delete_server_after", "0")
if move:
ac1.set_config("mvbox_move", "1")
ac1.bring_online()
ac1.stop_io()
ac1_direct_imap = direct_imap(ac1)
ac1_direct_imap.create_folder(folder)
# Wait until each folder was selected once and we are IDLEing:
ac1.start_io()
ac1.bring_online()
ac1.stop_io()
assert folder in ac1_direct_imap.list_folders()
log.section("Send a message from ac2 to ac1 and manually move it to `folder`")
ac1_direct_imap.select_config_folder("inbox")
with ac1_direct_imap.idle() as idle1:
acfactory.get_accepted_chat(ac2, ac1).send_text("hello")
idle1.wait_for_new_message()
ac1_direct_imap.conn.move(["*"], folder) # "*" means "biggest UID in mailbox"
log.section("start_io() and see if DeltaChat finds the message (" + variant + ")")
ac1.set_config("scan_all_folders_debounce_secs", "0")
ac1.start_io()
chat = ac1.create_chat(ac2)
n_msgs = 1 # "Messages are end-to-end encrypted."
if folder == "Spam":
msg = ac1.wait_for_incoming_msg().get_snapshot()
assert msg.text == "hello"
n_msgs += 1
else:
ac1.wait_for_event(EventType.IMAP_INBOX_IDLE)
assert len(chat.get_messages()) == n_msgs
# The message has reached its destination.
ac1_direct_imap.select_folder(expected_destination)
assert len(ac1_direct_imap.get_all_messages()) == 1
if folder != expected_destination:
ac1_direct_imap.select_folder(folder)
assert len(ac1_direct_imap.get_all_messages()) == 0
def test_trash_multiple_messages(acfactory, direct_imap, log):
ac1, ac2 = acfactory.get_online_accounts(2)
ac2.stop_io()
# Create the Trash folder on IMAP server and configure deletion to it. There was a bug that if
# Trash wasn't configured initially, it can't be configured later, let's check this.
log.section("Creating trash folder")
ac2_direct_imap = direct_imap(ac2)
ac2_direct_imap.create_folder("Trash")
ac2.set_config("delete_server_after", "0")
ac2.set_config("sync_msgs", "0")
ac2.set_config("delete_to_trash", "1")
log.section("Check that Trash can be configured initially as well")
ac3 = ac2.clone()
ac3.bring_online()
assert ac3.get_config("configured_trash_folder")
ac3.stop_io()
ac2.start_io()
chat12 = acfactory.get_accepted_chat(ac1, ac2)
@@ -509,17 +196,15 @@ def test_trash_multiple_messages(acfactory, direct_imap, log):
assert msg.text in texts
if text != "second":
to_delete.append(msg)
# ac2 has received some messages, this is impossible w/o the trash folder configured, let's
# check the configuration.
assert ac2.get_config("configured_trash_folder") == "Trash"
log.section("ac2: deleting all messages except second")
assert len(to_delete) == len(texts) - 1
ac2.delete_messages(to_delete)
log.section("ac2: test that only one message is left")
ac2_direct_imap = direct_imap(ac2)
while 1:
ac2.wait_for_event(EventType.IMAP_MESSAGE_MOVED)
ac2.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
ac2_direct_imap.select_config_folder("inbox")
nr_msgs = len(ac2_direct_imap.get_all_messages())
assert nr_msgs > 0
@@ -1,6 +1,7 @@
import pytest
from deltachat_rpc_client import EventType
from deltachat_rpc_client.const import DownloadState
from deltachat_rpc_client.rpc import JsonRpcError
@@ -37,8 +38,8 @@ def test_add_second_address(acfactory) -> None:
with pytest.raises(JsonRpcError):
account.set_config(option, "1")
with pytest.raises(JsonRpcError):
account.set_config("show_emails", "0")
# show_emails does not matter for multi-relay, can be set to anything
account.set_config("show_emails", "0")
@pytest.mark.parametrize("key", ["mvbox_move", "only_fetch_mvbox"])
@@ -57,8 +58,8 @@ def test_no_second_transport_with_mvbox(acfactory, key) -> None:
account.add_transport_from_qr(qr)
def test_no_second_transport_without_classic_emails(acfactory) -> None:
"""Test that second transport cannot be configured if classic emails are not fetched."""
def test_second_transport_without_classic_emails(acfactory) -> None:
"""Test that second transport can be configured if classic emails are not fetched."""
account = acfactory.new_configured_account()
assert len(account.list_transports()) == 1
@@ -67,8 +68,7 @@ def test_no_second_transport_without_classic_emails(acfactory) -> None:
qr = acfactory.get_account_qr()
account.set_config("show_emails", "0")
with pytest.raises(JsonRpcError):
account.add_transport_from_qr(qr)
account.add_transport_from_qr(qr)
def test_change_address(acfactory) -> None:
@@ -120,6 +120,33 @@ def test_change_address(acfactory) -> None:
assert sender_addr2 == new_alice_addr
def test_download_on_demand(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice.set_config("download_limit", "1")
alice.stop_io()
qr = acfactory.get_account_qr()
alice.add_transport_from_qr(qr)
alice.start_io()
alice.create_chat(bob)
chat_bob_alice = bob.create_chat(alice)
chat_bob_alice.send_message(file="../test-data/image/screenshot.jpg")
msg = alice.wait_for_incoming_msg()
snapshot = msg.get_snapshot()
assert snapshot.download_state == DownloadState.AVAILABLE
chat_id = snapshot.chat_id
# Actually the message isn't available yet. Wait somehow for the post-message to arrive.
chat_bob_alice.send_message("Now you can download my previous message")
alice.wait_for_incoming_msg()
alice._rpc.download_full_message(alice.id, msg.id)
for dstate in [DownloadState.IN_PROGRESS, DownloadState.DONE]:
event = alice.wait_for_event(EventType.MSGS_CHANGED)
assert event.chat_id == chat_id
assert event.msg_id == msg.id
assert msg.get_snapshot().download_state == dstate
@pytest.mark.parametrize("is_chatmail", ["0", "1"])
def test_mvbox_move_first_transport(acfactory, is_chatmail) -> None:
"""Test that mvbox_move is disabled by default even for non-chatmail accounts.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "2.39.0"
version = "2.42.0"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"
@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "2.39.0"
"version": "2.42.0"
}
-5
View File
@@ -17,11 +17,6 @@ ignore = [
# It is a transitive dependency of iroh 0.35.0,
# this should be fixed by upgrading to iroh 1.0 once it is released.
"RUSTSEC-2025-0134",
# Old versions of "lru" are transitive dependencies of iroh 0.35.0.
# <https://rustsec.org/advisories/RUSTSEC-2026-0002>
# <https://github.com/chatmail/core/issues/7692>
"RUSTSEC-2026-0002",
]
[bans]
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "2.39.0"
version = "2.42.0"
license = "MPL-2.0"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"
-1
View File
@@ -932,7 +932,6 @@ def test_set_get_group_image(acfactory, data, lp):
def test_connectivity(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("scan_all_folders_debounce_secs", "0")
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTED)
+1 -1
View File
@@ -1 +1 @@
2026-01-23
2026-02-10
+1 -1
View File
@@ -3,4 +3,4 @@
# Update package cache without changing the lockfile.
cargo update --dry-run
cargo deny --workspace --all-features check -D warnings
cargo deny --workspace --all-features --locked check -D warnings
+1 -1
View File
@@ -6,7 +6,7 @@ set -euo pipefail
export TZ=UTC
# Provider database revision.
REV=b21726d0c895ae43251579707229792e1fb4d837
REV=996c4bc82be5a7404f70b185ff062da33bfa98d9
CORE_ROOT="$PWD"
TMP="$(mktemp -d)"
+2 -2
View File
@@ -110,9 +110,9 @@ impl FromStr for Aheader {
SignedPublicKey::from_base64(&raw).context("autocrypt key cannot be decoded")
})
.and_then(|key| {
key.verify()
key.verify_bindings()
.and(Ok(key))
.context("autocrypt key cannot be verified")
.context("Autocrypt key cannot be verified")
})?;
let prefer_encrypt = attributes
+45 -37
View File
@@ -15,13 +15,12 @@ use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::net::dns::lookup_host_with_cache;
use crate::param::Param;
use crate::stock_str;
use crate::tools::{normalize_text, time};
use anyhow::{Context as _, Result, ensure};
use deltachat_derive::{FromSql, ToSql};
use num_traits::FromPrimitive;
use sdp::SessionDescription;
use serde::Serialize;
use std::io::Cursor;
use std::str::FromStr;
use std::time::Duration;
use tokio::task;
@@ -104,10 +103,14 @@ impl CallInfo {
};
if self.is_incoming() {
self.update_text(context, &format!("Incoming call\n{duration}"))
let incoming_call_str =
stock_str::incoming_call(context, self.has_video_initially()).await;
self.update_text(context, &format!("{incoming_call_str}\n{duration}"))
.await?;
} else {
self.update_text(context, &format!("Outgoing call\n{duration}"))
let outgoing_call_str =
stock_str::outgoing_call(context, self.has_video_initially()).await;
self.update_text(context, &format!("{outgoing_call_str}\n{duration}"))
.await?;
}
Ok(())
@@ -126,6 +129,14 @@ impl CallInfo {
self.msg.param.exists(CALL_ACCEPTED_TIMESTAMP)
}
/// Returns true if the call is started as a video call.
pub fn has_video_initially(&self) -> bool {
self.msg
.param
.get_bool(Param::WebrtcHasVideoInitially)
.unwrap_or(false)
}
/// Returns true if the call is missed
/// because the caller canceled it
/// explicitly before ringing stopped.
@@ -185,6 +196,7 @@ impl Context {
&self,
chat_id: ChatId,
place_call_info: String,
has_video_initially: bool,
) -> Result<MsgId> {
let chat = Chat::load_from_db(self, chat_id).await?;
ensure!(
@@ -193,12 +205,15 @@ impl Context {
);
ensure!(!chat.is_self_talk(), "Cannot call self");
let outgoing_call_str = stock_str::outgoing_call(self, has_video_initially).await;
let mut call = Message {
viewtype: Viewtype::Call,
text: "Outgoing call".into(),
text: outgoing_call_str,
..Default::default()
};
call.param.set(Param::WebrtcRoom, &place_call_info);
call.param
.set_int(Param::WebrtcHasVideoInitially, has_video_initially.into());
call.id = send_msg(self, chat_id, &mut call).await?;
let wait = RINGING_SECONDS;
@@ -266,10 +281,12 @@ impl Context {
if !call.is_accepted() {
if call.is_incoming() {
call.mark_as_ended(self).await?;
call.update_text(self, "Declined call").await?;
let declined_call_str = stock_str::declined_call(self).await;
call.update_text(self, &declined_call_str).await?;
} else {
call.mark_as_canceled(self).await?;
call.update_text(self, "Canceled call").await?;
let canceled_call_str = stock_str::canceled_call(self).await;
call.update_text(self, &canceled_call_str).await?;
}
} else {
call.mark_as_ended(self).await?;
@@ -311,10 +328,12 @@ impl Context {
if !call.is_accepted() && !call.is_ended() {
if call.is_incoming() {
call.mark_as_canceled(&context).await?;
call.update_text(&context, "Missed call").await?;
let missed_call_str = stock_str::missed_call(&context).await;
call.update_text(&context, &missed_call_str).await?;
} else {
call.mark_as_ended(&context).await?;
call.update_text(&context, "Canceled call").await?;
let canceled_call_str = stock_str::canceled_call(&context).await;
call.update_text(&context, &canceled_call_str).await?;
}
context.emit_msgs_changed(call.msg.chat_id, call_id);
context.emit_event(EventType::CallEnded {
@@ -339,18 +358,14 @@ impl Context {
if call.is_incoming() {
if call.is_stale() {
call.update_text(self, "Missed call").await?;
let missed_call_str = stock_str::missed_call(self).await;
call.update_text(self, &missed_call_str).await?;
self.emit_incoming_msg(call.msg.chat_id, call_id); // notify missed call
} else {
call.update_text(self, "Incoming call").await?;
let incoming_call_str =
stock_str::incoming_call(self, call.has_video_initially()).await;
call.update_text(self, &incoming_call_str).await?;
self.emit_msgs_changed(call.msg.chat_id, call_id); // ringing calls are not additionally notified
let has_video = match sdp_has_video(&call.place_call_info) {
Ok(has_video) => has_video,
Err(err) => {
warn!(self, "Failed to determine if SDP offer has video: {err:#}.");
false
}
};
let can_call_me = match who_can_call_me(self).await? {
WhoCanCallMe::Contacts => ChatIdBlocked::lookup_by_contact(self, from_id)
.await?
@@ -377,7 +392,7 @@ impl Context {
msg_id: call.msg.id,
chat_id: call.msg.chat_id,
place_call_info: call.place_call_info.to_string(),
has_video,
has_video: call.has_video_initially(),
});
}
let wait = call.remaining_ring_seconds();
@@ -389,7 +404,9 @@ impl Context {
));
}
} else {
call.update_text(self, "Outgoing call").await?;
let outgoing_call_str =
stock_str::outgoing_call(self, call.has_video_initially()).await;
call.update_text(self, &outgoing_call_str).await?;
self.emit_msgs_changed(call.msg.chat_id, call_id);
}
} else {
@@ -439,19 +456,23 @@ impl Context {
if call.is_incoming() {
if from_id == ContactId::SELF {
call.mark_as_ended(self).await?;
call.update_text(self, "Declined call").await?;
let declined_call_str = stock_str::declined_call(self).await;
call.update_text(self, &declined_call_str).await?;
} else {
call.mark_as_canceled(self).await?;
call.update_text(self, "Missed call").await?;
let missed_call_str = stock_str::missed_call(self).await;
call.update_text(self, &missed_call_str).await?;
}
} else {
// outgoing
if from_id == ContactId::SELF {
call.mark_as_canceled(self).await?;
call.update_text(self, "Canceled call").await?;
let canceled_call_str = stock_str::canceled_call(self).await;
call.update_text(self, &canceled_call_str).await?;
} else {
call.mark_as_ended(self).await?;
call.update_text(self, "Declined call").await?;
let declined_call_str = stock_str::declined_call(self).await;
call.update_text(self, &declined_call_str).await?;
}
}
} else {
@@ -507,19 +528,6 @@ impl Context {
}
}
/// Returns true if SDP offer has a video.
pub fn sdp_has_video(sdp: &str) -> Result<bool> {
let mut cursor = Cursor::new(sdp);
let session_description =
SessionDescription::unmarshal(&mut cursor).context("Failed to parse SDP")?;
for media_description in &session_description.media_descriptions {
if media_description.media_name.media == "video" {
return Ok(true);
}
}
Ok(false)
}
/// State of the call for display in the message bubble.
#[derive(Debug, PartialEq, Eq)]
pub enum CallState {
+19 -31
View File
@@ -25,13 +25,6 @@ async fn assert_text(t: &TestContext, call_id: MsgId, text: &str) -> Result<()>
const PLACE_INFO: &str = "v=0\r\no=alice 2890844526 2890844526 IN IP4 host.anywhere.com\r\ns=-\r\nc=IN IP4 host.anywhere.com\r\nt=0 0\r\nm=audio 62986 RTP/AVP 0 4 18\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:4 G723/8000\r\na=rtpmap:18 G729/8000\r\na=inactive\r\n";
const ACCEPT_INFO: &str = "v=0\r\no=bob 2890844730 2890844731 IN IP4 host.example.com\r\ns=\r\nc=IN IP4 host.example.com\r\nt=0 0\r\nm=audio 54344 RTP/AVP 0 4\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:4 G723/8000\r\na=inactive\r\n";
/// Example from <https://datatracker.ietf.org/doc/rfc9143/>
/// with `s= ` replaced with `s=-`.
///
/// `s=` cannot be empty according to RFC 3264,
/// so it is more clear as `s=-`.
const PLACE_INFO_VIDEO: &str = "v=0\r\no=alice 2890844526 2890844526 IN IP6 2001:db8::3\r\ns=-\r\nc=IN IP6 2001:db8::3\r\nt=0 0\r\na=group:BUNDLE foo bar\r\n\r\nm=audio 10000 RTP/AVP 0 8 97\r\nb=AS:200\r\na=mid:foo\r\na=rtcp-mux\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:97 iLBC/8000\r\na=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r\n\r\nm=video 10002 RTP/AVP 31 32\r\nb=AS:1000\r\na=mid:bar\r\na=rtcp-mux\r\na=rtpmap:31 H261/90000\r\na=rtpmap:32 MPV/90000\r\na=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r\n";
async fn setup_call() -> Result<CallSetup> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
@@ -52,7 +45,7 @@ async fn setup_call() -> Result<CallSetup> {
bob2.create_chat(&alice).await;
let test_msg_id = alice
.place_outgoing_call(alice_chat.id, PLACE_INFO.to_string())
.place_outgoing_call(alice_chat.id, PLACE_INFO.to_string(), true)
.await?;
let sent1 = alice.pop_sent_msg().await;
assert_eq!(sent1.sender_msg_id, test_msg_id);
@@ -68,7 +61,8 @@ async fn setup_call() -> Result<CallSetup> {
assert!(!info.is_incoming());
assert!(!info.is_accepted());
assert_eq!(info.place_call_info, PLACE_INFO);
assert_text(t, m.id, "Outgoing call").await?;
assert_eq!(info.has_video_initially(), true);
assert_text(t, m.id, "Outgoing video call").await?;
assert_eq!(call_state(t, m.id).await?, CallState::Alerting);
}
@@ -89,7 +83,8 @@ async fn setup_call() -> Result<CallSetup> {
assert!(info.is_incoming());
assert!(!info.is_accepted());
assert_eq!(info.place_call_info, PLACE_INFO);
assert_text(t, m.id, "Incoming call").await?;
assert_eq!(info.has_video_initially(), true);
assert_text(t, m.id, "Incoming video call").await?;
assert_eq!(call_state(t, m.id).await?, CallState::Alerting);
}
@@ -120,7 +115,7 @@ async fn accept_call() -> Result<CallSetup> {
// Bob accepts the incoming call
bob.accept_incoming_call(bob_call.id, ACCEPT_INFO.to_string())
.await?;
assert_text(&bob, bob_call.id, "Incoming call").await?;
assert_text(&bob, bob_call.id, "Incoming video call").await?;
bob.evtracker
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
.await;
@@ -134,7 +129,7 @@ async fn accept_call() -> Result<CallSetup> {
assert_eq!(call_state(&bob, bob_call.id).await?, CallState::Active);
bob2.recv_msg_trash(&sent2).await;
assert_text(&bob, bob_call.id, "Incoming call").await?;
assert_text(&bob, bob_call.id, "Incoming video call").await?;
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
.await;
@@ -147,7 +142,7 @@ async fn accept_call() -> Result<CallSetup> {
// Alice receives the acceptance message
alice.recv_msg_trash(&sent2).await;
assert_text(&alice, alice_call.id, "Outgoing call").await?;
assert_text(&alice, alice_call.id, "Outgoing video call").await?;
let ev = alice
.evtracker
.get_matching(|evt| matches!(evt, EventType::OutgoingCallAccepted { .. }))
@@ -169,7 +164,7 @@ async fn accept_call() -> Result<CallSetup> {
assert_eq!(call_state(&alice, alice_call.id).await?, CallState::Active);
alice2.recv_msg_trash(&sent2).await;
assert_text(&alice2, alice2_call.id, "Outgoing call").await?;
assert_text(&alice2, alice2_call.id, "Outgoing video call").await?;
alice2
.evtracker
.get_matching(|evt| matches!(evt, EventType::OutgoingCallAccepted { .. }))
@@ -208,7 +203,7 @@ async fn test_accept_call_callee_ends() -> Result<()> {
// Bob has accepted the call and also ends it
bob.end_call(bob_call.id).await?;
assert_text(&bob, bob_call.id, "Incoming call\n<1 minute").await?;
assert_text(&bob, bob_call.id, "Incoming video call\n<1 minute").await?;
bob.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
@@ -219,7 +214,7 @@ async fn test_accept_call_callee_ends() -> Result<()> {
));
bob2.recv_msg_trash(&sent3).await;
assert_text(&bob2, bob2_call.id, "Incoming call\n<1 minute").await?;
assert_text(&bob2, bob2_call.id, "Incoming video call\n<1 minute").await?;
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
@@ -230,7 +225,7 @@ async fn test_accept_call_callee_ends() -> Result<()> {
// Alice receives the ending message
alice.recv_msg_trash(&sent3).await;
assert_text(&alice, alice_call.id, "Outgoing call\n<1 minute").await?;
assert_text(&alice, alice_call.id, "Outgoing video call\n<1 minute").await?;
alice
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
@@ -241,7 +236,7 @@ async fn test_accept_call_callee_ends() -> Result<()> {
));
alice2.recv_msg_trash(&sent3).await;
assert_text(&alice2, alice2_call.id, "Outgoing call\n<1 minute").await?;
assert_text(&alice2, alice2_call.id, "Outgoing video call\n<1 minute").await?;
alice2
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
@@ -271,7 +266,7 @@ async fn test_accept_call_caller_ends() -> Result<()> {
// Bob has accepted the call but Alice ends it
alice.end_call(alice_call.id).await?;
assert_text(&alice, alice_call.id, "Outgoing call\n<1 minute").await?;
assert_text(&alice, alice_call.id, "Outgoing video call\n<1 minute").await?;
alice
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
@@ -283,7 +278,7 @@ async fn test_accept_call_caller_ends() -> Result<()> {
));
alice2.recv_msg_trash(&sent3).await;
assert_text(&alice2, alice2_call.id, "Outgoing call\n<1 minute").await?;
assert_text(&alice2, alice2_call.id, "Outgoing video call\n<1 minute").await?;
alice2
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
@@ -295,7 +290,7 @@ async fn test_accept_call_caller_ends() -> Result<()> {
// Bob receives the ending message
bob.recv_msg_trash(&sent3).await;
assert_text(&bob, bob_call.id, "Incoming call\n<1 minute").await?;
assert_text(&bob, bob_call.id, "Incoming video call\n<1 minute").await?;
bob.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
@@ -305,7 +300,7 @@ async fn test_accept_call_caller_ends() -> Result<()> {
));
bob2.recv_msg_trash(&sent3).await;
assert_text(&bob2, bob2_call.id, "Incoming call\n<1 minute").await?;
assert_text(&bob2, bob2_call.id, "Incoming video call\n<1 minute").await?;
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
@@ -425,7 +420,7 @@ async fn test_caller_cancels_call() -> Result<()> {
// Test that message summary says it is a missed call.
let bob_call_msg = Message::load_from_db(&bob, bob_call.id).await?;
let summary = bob_call_msg.get_summary(&bob, None).await?;
assert_eq!(summary.text, "📞 Missed call");
assert_eq!(summary.text, "🎥 Missed call");
bob2.recv_msg_trash(&sent3).await;
assert_text(&bob2, bob2_call.id, "Missed call").await?;
@@ -525,13 +520,6 @@ async fn test_update_call_text() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sdp_has_video() {
assert!(sdp_has_video("foobar").is_err());
assert_eq!(sdp_has_video(PLACE_INFO).unwrap(), false);
assert_eq!(sdp_has_video(PLACE_INFO_VIDEO).unwrap(), true);
}
/// Tests that calls are forwarded as text messages.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_forward_call() -> Result<()> {
@@ -542,7 +530,7 @@ async fn test_forward_call() -> Result<()> {
let alice_bob_chat = alice.create_chat(bob).await;
let alice_msg_id = alice
.place_outgoing_call(alice_bob_chat.id, PLACE_INFO.to_string())
.place_outgoing_call(alice_bob_chat.id, PLACE_INFO.to_string(), true)
.await
.context("Failed to place a call")?;
let alice_call = Message::load_from_db(alice, alice_msg_id).await?;
+16 -12
View File
@@ -618,7 +618,6 @@ impl ChatId {
);
let chat = Chat::load_from_db(context, self).await?;
let delete_msgs_target = context.get_delete_msgs_target().await?;
let sync_id = match sync {
Nosync => None,
Sync => chat.get_sync_id(context).await?,
@@ -628,12 +627,12 @@ impl ChatId {
.sql
.transaction(|transaction| {
transaction.execute(
"UPDATE imap SET target=? WHERE rfc724_mid IN (SELECT rfc724_mid FROM msgs WHERE chat_id=? AND rfc724_mid!='')",
(&delete_msgs_target, self,),
"UPDATE imap SET target='' WHERE rfc724_mid IN (SELECT rfc724_mid FROM msgs WHERE chat_id=? AND rfc724_mid!='')",
(self,),
)?;
transaction.execute(
"UPDATE imap SET target=? WHERE rfc724_mid IN (SELECT pre_rfc724_mid FROM msgs WHERE chat_id=? AND pre_rfc724_mid!='')",
(&delete_msgs_target, self,),
"UPDATE imap SET target='' WHERE rfc724_mid IN (SELECT pre_rfc724_mid FROM msgs WHERE chat_id=? AND pre_rfc724_mid!='')",
(self,),
)?;
transaction.execute(
"DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?)",
@@ -1886,11 +1885,7 @@ impl Chat {
let (msg_text, was_truncated) = truncate_msg_text(context, msg.text.clone()).await?;
let new_mime_headers = if msg.has_html() {
if msg.param.exists(Param::Forwarded) {
msg.get_id().get_html(context).await?
} else {
msg.param.get(Param::SendHtml).map(|s| s.to_string())
}
msg.param.get(Param::SendHtml).map(|s| s.to_string())
} else {
None
};
@@ -4367,8 +4362,12 @@ pub async fn forward_msgs_2ctx(
msg.param = Params::new();
if msg.get_viewtype() != Viewtype::Sticker {
let forwarded_msg_id = match ctx_src.blobdir == ctx_dst.blobdir {
true => src_msg_id,
false => MsgId::new_unset(),
};
msg.param
.set_int(Param::Forwarded, src_msg_id.to_u32() as i32);
.set_int(Param::Forwarded, forwarded_msg_id.to_u32() as i32);
}
if msg.get_viewtype() == Viewtype::Call {
@@ -4380,7 +4379,9 @@ pub async fn forward_msgs_2ctx(
// When forwarding between different accounts, blob files must be physically copied
// because each account has its own blob directory.
if let Some(src_path) = param.get_file_path(ctx_src)? {
if ctx_src.blobdir == ctx_dst.blobdir {
msg.param.steal(param, Param::File);
} else if let Some(src_path) = param.get_file_path(ctx_src)? {
let new_blob = BlobObject::create_and_deduplicate(ctx_dst, &src_path, &src_path)
.context("Failed to copy blob file to destination account")?;
msg.param.set(Param::File, new_blob.as_name());
@@ -4393,6 +4394,9 @@ pub async fn forward_msgs_2ctx(
msg.param.steal(param, Param::ProtectQuote);
msg.param.steal(param, Param::Quote);
msg.param.steal(param, Param::Summary1);
if msg.has_html() {
msg.set_html(src_msg_id.get_html(ctx_src).await?);
}
msg.in_reply_to = None;
// do not leak data as group names; a default subject is generated by mimefactory
+2 -20
View File
@@ -194,10 +194,6 @@ pub enum Config {
#[strum(props(default = "0"))]
DeleteDeviceAfter,
/// Move messages to the Trash folder instead of marking them "\Deleted". Overrides
/// `ProviderOptions::delete_to_trash`.
DeleteToTrash,
/// The primary email address.
ConfiguredAddr,
@@ -275,9 +271,6 @@ pub enum Config {
/// Configured folder for chat messages.
ConfiguredMvboxFolder,
/// Configured "Trash" folder.
ConfiguredTrashFolder,
/// Unix timestamp of the last successful configuration.
ConfiguredTimestamp,
@@ -335,10 +328,6 @@ pub enum Config {
/// Timestamp of the last `CantDecryptOutgoingMsgs` notification.
LastCantDecryptOutgoingMsgs,
/// To how many seconds to debounce scan_all_folders. Used mainly in tests, to disable debouncing completely.
#[strum(props(default = "60"))]
ScanAllFoldersDebounceSecs,
/// Whether to avoid using IMAP IDLE even if the server supports it.
///
/// This is a developer option for testing "fake idle".
@@ -602,8 +591,7 @@ impl Context {
.get_config(key)
.await?
.and_then(|s| s.parse::<i32>().ok())
.map(|x| x != 0)
.unwrap_or_default())
.is_some_and(|x| x != 0))
}
/// Returns true if movebox ("DeltaChat" folder) should be watched.
@@ -696,7 +684,6 @@ impl Context {
| Config::MdnsEnabled
| Config::MvboxMove
| Config::OnlyFetchMvbox
| Config::DeleteToTrash
| Config::Configured
| Config::Bot
| Config::NotifyAboutWrongPw
@@ -720,12 +707,7 @@ impl Context {
Self::check_config(key, value)?;
let n_transports = self.count_transports().await?;
if n_transports > 1
&& matches!(
key,
Config::MvboxMove | Config::OnlyFetchMvbox | Config::ShowEmails
)
{
if n_transports > 1 && matches!(key, Config::MvboxMove | Config::OnlyFetchMvbox) {
bail!("Cannot reconfigure {key} when multiple transports are configured");
}
-9
View File
@@ -124,10 +124,6 @@ impl Context {
}
pub(crate) async fn add_transport_inner(&self, param: &mut EnteredLoginParam) -> Result<()> {
ensure!(
!self.scheduler.is_running().await,
"cannot configure, already running"
);
ensure!(
self.sql.is_open().await,
"cannot configure, database not opened."
@@ -286,11 +282,6 @@ impl Context {
"To use additional relays, disable the legacy option \"Settings / Advanced / Move automatically to DeltaChat Folder\"."
);
}
if self.get_config(Config::ShowEmails).await?.as_deref() != Some("2") {
bail!(
"To use additional relays, set the legacy option \"Settings / Advanced / Show Classic Emails\" to \"All\"."
);
}
if self
.sql
+1 -45
View File
@@ -8,7 +8,7 @@ use std::sync::atomic::AtomicBool;
use std::sync::{Arc, OnceLock, Weak};
use std::time::Duration;
use anyhow::{Context as _, Result, bail, ensure};
use anyhow::{Result, bail, ensure};
use async_channel::{self as channel, Receiver, Sender};
use ratelimit::Ratelimit;
use tokio::sync::{Mutex, Notify, RwLock};
@@ -880,10 +880,6 @@ impl Context {
.get_config(Config::ConfiguredMvboxFolder)
.await?
.unwrap_or_else(|| "<unset>".to_string());
let configured_trash_folder = self
.get_config(Config::ConfiguredTrashFolder)
.await?
.unwrap_or_else(|| "<unset>".to_string());
let mut res = get_info();
@@ -968,7 +964,6 @@ impl Context {
);
res.insert("configured_inbox_folder", configured_inbox_folder);
res.insert("configured_mvbox_folder", configured_mvbox_folder);
res.insert("configured_trash_folder", configured_trash_folder);
res.insert("mdns_enabled", mdns_enabled.to_string());
res.insert("bcc_self", bcc_self.to_string());
res.insert("sync_msgs", sync_msgs.to_string());
@@ -992,12 +987,6 @@ impl Context {
.await?
.to_string(),
);
res.insert(
"delete_to_trash",
self.get_config(Config::DeleteToTrash)
.await?
.unwrap_or_else(|| "<unset>".to_string()),
);
res.insert(
"last_housekeeping",
self.get_config_int(Config::LastHousekeeping)
@@ -1010,12 +999,6 @@ impl Context {
.await?
.to_string(),
);
res.insert(
"scan_all_folders_debounce_secs",
self.get_config_int(Config::ScanAllFoldersDebounceSecs)
.await?
.to_string(),
);
res.insert(
"quota_exceeding",
self.get_config_int(Config::QuotaExceeding)
@@ -1286,33 +1269,6 @@ ORDER BY m.timestamp DESC,m.id DESC",
Ok(mvbox.as_deref() == Some(folder_name))
}
/// Returns true if given folder name is the name of the trash folder.
pub async fn is_trash(&self, folder_name: &str) -> Result<bool> {
let trash = self.get_config(Config::ConfiguredTrashFolder).await?;
Ok(trash.as_deref() == Some(folder_name))
}
pub(crate) async fn should_delete_to_trash(&self) -> Result<bool> {
if let Some(v) = self.get_config_bool_opt(Config::DeleteToTrash).await? {
return Ok(v);
}
if let Some(provider) = self.get_configured_provider().await? {
return Ok(provider.opt.delete_to_trash);
}
Ok(false)
}
/// Returns `target` for deleted messages as per `imap` table. Empty string means "delete w/o
/// moving to trash".
pub(crate) async fn get_delete_msgs_target(&self) -> Result<String> {
if !self.should_delete_to_trash().await? {
return Ok("".into());
}
self.get_config(Config::ConfiguredTrashFolder)
.await?
.context("No configured trash folder")
}
pub(crate) fn derive_blobdir(dbfile: &Path) -> PathBuf {
let mut blob_fname = OsString::new();
blob_fname.push(dbfile.file_name().unwrap_or_default());
+19 -14
View File
@@ -99,7 +99,8 @@ impl MsgId {
Ok(())
}
/// Updates the message download state. Returns `Ok` if the message doesn't exist anymore.
/// Updates the message download state. Returns `Ok` if the message doesn't exist anymore or has
/// the download state up to date.
pub(crate) async fn update_download_state(
self,
context: &Context,
@@ -108,7 +109,7 @@ impl MsgId {
if context
.sql
.execute(
"UPDATE msgs SET download_state=? WHERE id=?;",
"UPDATE msgs SET download_state=? WHERE id=? AND download_state<>?1",
(download_state, self),
)
.await?
@@ -135,42 +136,46 @@ impl Message {
}
}
/// Actually download a message partially downloaded before.
/// Actually downloads a message partially downloaded before if the message is available on the
/// session transport, in which case returns `Some`. If the message is available on another
/// transport, returns `None`.
///
/// Most messages are downloaded automatically on fetch instead.
pub(crate) async fn download_msg(
context: &Context,
rfc724_mid: String,
session: &mut Session,
) -> Result<()> {
) -> Result<Option<()>> {
let transport_id = session.transport_id();
let row = context
.sql
.query_row_optional(
"SELECT uid, folder FROM imap
WHERE rfc724_mid=?
AND transport_id=?
AND target!=''",
"SELECT uid, folder, transport_id FROM imap
WHERE rfc724_mid=? AND target!=''
ORDER BY transport_id=? DESC LIMIT 1",
(&rfc724_mid, transport_id),
|row| {
let server_uid: u32 = row.get(0)?;
let server_folder: String = row.get(1)?;
Ok((server_uid, server_folder))
let msg_transport_id: u32 = row.get(2)?;
Ok((server_uid, server_folder, msg_transport_id))
},
)
.await?;
let Some((server_uid, server_folder)) = row else {
let Some((server_uid, server_folder, msg_transport_id)) = row else {
// No IMAP record found, we don't know the UID and folder.
return Err(anyhow!(
"IMAP location for {rfc724_mid:?} post-message is unknown"
));
};
if msg_transport_id != transport_id {
return Ok(None);
}
session
.fetch_single_msg(context, &server_folder, server_uid, rfc724_mid)
.await?;
Ok(())
Ok(Some(()))
}
impl Session {
@@ -272,7 +277,7 @@ pub(crate) async fn download_msgs(context: &Context, session: &mut Session) -> R
for rfc724_mid in &rfc724_mids {
let res = download_msg(context, rfc724_mid.clone(), session).await;
if res.is_ok() {
if let Ok(Some(())) = res {
delete_from_downloads(context, rfc724_mid).await?;
delete_from_available_post_msgs(context, rfc724_mid).await?;
}
@@ -327,7 +332,7 @@ pub(crate) async fn download_known_post_messages_without_pre_message(
// The message may be in the wrong order,
// but at least we have it at all.
let res = download_msg(context, rfc724_mid.clone(), session).await;
if res.is_ok() {
if let Ok(Some(())) = res {
delete_from_available_post_msgs(context, rfc724_mid).await?;
}
if let Err(err) = res {
+2 -8
View File
@@ -665,25 +665,19 @@ pub(crate) async fn delete_expired_imap_messages(context: &Context) -> Result<()
now - max(delete_server_after, MIN_DELETE_SERVER_AFTER),
),
};
let target = context.get_delete_msgs_target().await?;
context
.sql
.execute(
"UPDATE imap
SET target=?
SET target=''
WHERE rfc724_mid IN (
SELECT rfc724_mid FROM msgs
WHERE ((download_state = 0 AND timestamp < ?) OR
(download_state != 0 AND timestamp < ?) OR
(ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?))
)",
(
&target,
threshold_timestamp,
threshold_timestamp_extended,
now,
),
(threshold_timestamp, threshold_timestamp_extended, now),
)
.await?;
+1 -1
View File
@@ -71,7 +71,7 @@ impl EventEmitter {
/// [`try_recv`]: Self::try_recv
pub async fn recv(&self) -> Option<Event> {
let mut lock = self.0.lock().await;
match lock.recv().await {
match lock.recv_direct().await {
Err(async_broadcast::RecvError::Overflowed(n)) => Some(Event {
id: 0,
typ: EventType::EventChannelOverflow { n },
+1
View File
@@ -91,6 +91,7 @@ pub enum HeaderDef {
ChatDispositionNotificationTo,
ChatWebrtcRoom,
ChatWebrtcAccepted,
ChatWebrtcHasVideoInitially,
/// This message deletes the messages listed in the value by rfc724_mid.
ChatDelete,
+59 -26
View File
@@ -254,13 +254,20 @@ fn mimepart_to_data_url(mail: &mailparse::ParsedMail<'_>) -> Result<String> {
impl MsgId {
/// Get HTML by database message id.
/// This requires `mime_headers` field to be set for the message;
/// this is the case at least when `Message.has_html()` returns true
/// (we do not save raw mime unconditionally in the database to save space).
/// Returns `Some` at least if `Message.has_html()` is true.
/// NB: we do not save raw mime unconditionally in the database to save space.
/// The corresponding ffi-function is `dc_get_msg_html()`.
pub async fn get_html(self, context: &Context) -> Result<Option<String>> {
let rawmime = message::get_mime_headers(context, self).await?;
// If there are many concurrent db readers, going to the queue earlier makes sense.
let (param, rawmime) = tokio::join!(
self.get_param(context),
message::get_mime_headers(context, self)
);
if let Some(html) = param?.get(SendHtml) {
return Ok(Some(html.to_string()));
}
let rawmime = rawmime?;
if !rawmime.is_empty() {
match HtmlMsgParser::from_bytes(context, &rawmime).await {
Err(err) => {
@@ -279,9 +286,9 @@ impl MsgId {
#[cfg(test)]
mod tests {
use super::*;
use crate::chat;
use crate::chat::{forward_msgs, save_msgs};
use crate::chat::{self, Chat, forward_msgs, save_msgs};
use crate::config::Config;
use crate::constants;
use crate::contact::ContactId;
use crate::message::{MessengerMessage, Viewtype};
use crate::receive_imf::receive_imf;
@@ -440,7 +447,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_html_forwarding() {
async fn test_html_forwarding() -> Result<()> {
// alice receives a non-delta html-message
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
@@ -459,31 +466,57 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert!(html.contains("this is <b>html</b>"));
// alice: create chat with bob and forward received html-message there
let chat = alice.create_chat_with_contact("", "bob@example.net").await;
forward_msgs(alice, &[msg.get_id()], chat.get_id())
let chat_alice = alice.create_chat_with_contact("", "bob@example.net").await;
forward_msgs(alice, &[msg.get_id()], chat_alice.get_id())
.await
.unwrap();
let msg = alice.get_last_msg_in(chat.get_id()).await;
assert_eq!(msg.get_from_id(), ContactId::SELF);
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
assert!(msg.is_forwarded());
assert!(msg.get_text().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(alice).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
async fn check_sender(ctx: &TestContext, chat: &Chat) {
let msg = ctx.get_last_msg_in(chat.get_id()).await;
assert_eq!(msg.get_from_id(), ContactId::SELF);
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
assert!(msg.is_forwarded());
assert!(msg.get_text().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(ctx).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
}
check_sender(alice, &chat_alice).await;
// bob: check that bob also got the html-part of the forwarded message
let bob = &tcm.bob().await;
let chat = bob.create_chat_with_contact("", "alice@example.org").await;
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
assert_eq!(chat.id, msg.chat_id);
assert_ne!(msg.get_from_id(), ContactId::SELF);
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
assert!(msg.is_forwarded());
assert!(msg.get_text().contains("this is plain"));
let chat_bob = bob.create_chat_with_contact("", "alice@example.org").await;
async fn check_receiver(ctx: &TestContext, chat: &Chat, sender: &TestContext) {
let msg = ctx.recv_msg(&sender.pop_sent_msg().await).await;
assert_eq!(chat.id, msg.chat_id);
assert_ne!(msg.get_from_id(), ContactId::SELF);
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
assert!(msg.is_forwarded());
assert!(msg.get_text().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(ctx).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
}
check_receiver(bob, &chat_bob, alice).await;
// Let's say that the alice and bob profiles are on the same device,
// so alice can forward the message to herself via bob profile!
chat::forward_msgs_2ctx(alice, &[msg.get_id()], bob, chat_bob.get_id()).await?;
check_sender(bob, &chat_bob).await;
check_receiver(alice, &chat_alice, bob).await;
// Check cross-profile forwarding of long outgoing messages.
let line = "this text with 42 chars is just repeated.\n";
let long_txt = line.repeat(constants::DC_DESIRED_TEXT_LEN / line.len() + 2);
let mut msg = Message::new_text(long_txt);
alice.send_msg(chat_alice.id, &mut msg).await;
let msg = alice.get_last_msg_in(chat_alice.id).await;
assert!(msg.has_html());
let html = msg.get_id().get_html(bob).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
let html = msg.id.get_html(alice).await?.unwrap();
chat::forward_msgs_2ctx(alice, &[msg.get_id()], bob, chat_bob.get_id()).await?;
let msg = bob.get_last_msg_in(chat_bob.id).await;
assert!(msg.has_html());
assert_eq!(msg.id.get_html(bob).await?.unwrap(), html);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+30 -36
View File
@@ -53,7 +53,6 @@ use crate::transport::{
pub(crate) mod capabilities;
mod client;
mod idle;
pub mod scan_folders;
pub mod select_folder;
pub(crate) mod session;
@@ -183,7 +182,7 @@ impl FolderMeaning {
FolderMeaning::Spam => None,
FolderMeaning::Inbox => Some(Config::ConfiguredInboxFolder),
FolderMeaning::Mvbox => Some(Config::ConfiguredMvboxFolder),
FolderMeaning::Trash => Some(Config::ConfiguredTrashFolder),
FolderMeaning::Trash => None,
FolderMeaning::Virtual => None,
}
}
@@ -611,7 +610,6 @@ impl Imap {
let mut download_later: Vec<String> = Vec::new();
let mut uid_message_ids = BTreeMap::new();
let mut largest_uid_skipped = None;
let delete_target = context.get_delete_msgs_target().await?;
let download_limit: Option<u32> = context
.get_config_parsed(Config::DownloadLimit)
@@ -661,7 +659,7 @@ impl Imap {
let _target;
let target = if delete {
&delete_target
""
} else {
_target = target_folder(context, folder, folder_meaning, &headers).await?;
&_target
@@ -988,17 +986,6 @@ impl Session {
return Ok(());
}
Err(err) => {
if context.should_delete_to_trash().await? {
error!(
context,
"Cannot move messages {} to {}, no fallback to COPY/DELETE because \
delete_to_trash is set. Error: {:#}",
set,
target,
err,
);
return Err(err.into());
}
warn!(
context,
"Cannot move messages, fallback to COPY/DELETE {} to {}: {}",
@@ -1012,19 +999,11 @@ impl Session {
// Server does not support MOVE or MOVE failed.
// Copy messages to the destination folder if needed and mark records for deletion.
let copy = !context.is_trash(target).await?;
if copy {
info!(
context,
"Server does not support MOVE, fallback to COPY/DELETE {} to {}", set, target
);
self.uid_copy(&set, &target).await?;
} else {
error!(
context,
"Server does not support MOVE, fallback to DELETE {} to {}", set, target,
);
}
info!(
context,
"Server does not support MOVE, fallback to COPY/DELETE {} to {}", set, target
);
self.uid_copy(&set, &target).await?;
context
.sql
.transaction(|transaction| {
@@ -1036,11 +1015,9 @@ impl Session {
})
.await
.context("Cannot plan deletion of messages")?;
if copy {
context.emit_event(EventType::ImapMessageMoved(format!(
"IMAP messages {set} copied to {target}"
)));
}
context.emit_event(EventType::ImapMessageMoved(format!(
"IMAP messages {set} copied to {target}"
)));
Ok(())
}
@@ -2175,11 +2152,10 @@ pub(crate) async fn prefetch_should_download(
let accepted_contact = origin.is_known();
let is_reply_to_chat_message = get_prefetch_parent_message(context, headers)
.await?
.map(|parent| match parent.is_dc_message {
.is_some_and(|parent| match parent.is_dc_message {
MessengerMessage::No => false,
MessengerMessage::Yes | MessengerMessage::Reply => true,
})
.unwrap_or_default();
});
let show_emails =
ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default();
@@ -2443,5 +2419,23 @@ impl std::fmt::Display for UidRange {
}
}
pub(crate) async fn get_watched_folder_configs(context: &Context) -> Result<Vec<Config>> {
let mut res = vec![Config::ConfiguredInboxFolder];
if context.should_watch_mvbox().await? {
res.push(Config::ConfiguredMvboxFolder);
}
Ok(res)
}
pub(crate) async fn get_watched_folders(context: &Context) -> Result<Vec<String>> {
let mut res = Vec::new();
for folder_config in get_watched_folder_configs(context).await? {
if let Some(folder) = context.get_config(folder_config).await? {
res.push(folder);
}
}
Ok(res)
}
#[cfg(test)]
mod imap_tests;
-119
View File
@@ -1,119 +0,0 @@
use std::collections::BTreeMap;
use anyhow::{Context as _, Result};
use super::{get_folder_meaning_by_attrs, get_folder_meaning_by_name};
use crate::config::Config;
use crate::imap::{Imap, session::Session};
use crate::log::LogExt;
use crate::tools::{self, time_elapsed};
use crate::{context::Context, imap::FolderMeaning};
impl Imap {
/// Returns true if folders were scanned, false if scanning was postponed.
pub(crate) async fn scan_folders(
&mut self,
context: &Context,
session: &mut Session,
) -> Result<bool> {
// First of all, debounce to once per minute:
{
let mut last_scan = session.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);
}
}
// 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");
let folders = session.list_folders().await?;
let watched_folders = get_watched_folders(context).await?;
let mut folder_configs = BTreeMap::new();
let mut folder_names = Vec::new();
for folder in folders {
let folder_meaning = get_folder_meaning_by_attrs(folder.attributes());
if folder_meaning == FolderMeaning::Virtual {
// Gmail has virtual folders that should be skipped. For example,
// emails appear in the inbox and under "All Mail" as soon as it is
// received. The code used to wrongly conclude that the email had
// already been moved and left it in the inbox.
continue;
}
folder_names.push(folder.name().to_string());
let folder_name_meaning = get_folder_meaning_by_name(folder.name());
if let Some(config) = folder_meaning.to_config() {
// Always takes precedence
folder_configs.insert(config, folder.name().to_string());
} else if let Some(config) = folder_name_meaning.to_config() {
// only set if none has been already set
folder_configs
.entry(config)
.or_insert_with(|| folder.name().to_string());
}
let folder_meaning = match folder_meaning {
FolderMeaning::Unknown => folder_name_meaning,
_ => folder_meaning,
};
// Don't scan folders that are watched anyway
if !watched_folders.contains(&folder.name().to_string())
&& folder_meaning != FolderMeaning::Trash
&& folder_meaning != FolderMeaning::Unknown
{
self.fetch_move_delete(context, session, folder.name(), folder_meaning)
.await
.context("Can't fetch new msgs in scanned folder")
.log_err(context)
.ok();
}
}
// Set config for the Trash folder. Or reset if the folder was deleted.
let conf = Config::ConfiguredTrashFolder;
let val = folder_configs.get(&conf).map(|s| s.as_str());
let interrupt = val.is_some() && context.get_config(conf).await?.is_none();
context.set_config_internal(conf, val).await?;
if interrupt {
// `Imap::fetch_move_delete()`, particularly message deletion, is possible now for other
// folders (NB: we are in the Inbox loop).
context.scheduler.interrupt_oboxes().await;
}
info!(context, "Found folders: {folder_names:?}.");
Ok(true)
}
}
pub(crate) async fn get_watched_folder_configs(context: &Context) -> Result<Vec<Config>> {
let mut res = vec![Config::ConfiguredInboxFolder];
if context.should_watch_mvbox().await? {
res.push(Config::ConfiguredMvboxFolder);
}
Ok(res)
}
pub(crate) async fn get_watched_folders(context: &Context) -> Result<Vec<String>> {
let mut res = Vec::new();
for folder_config in get_watched_folder_configs(context).await? {
if let Some(folder) = context.get_config(folder_config).await? {
res.push(folder);
}
}
Ok(res)
}
-5
View File
@@ -5,11 +5,9 @@ use anyhow::{Context as _, Result};
use async_imap::Session as ImapSession;
use async_imap::types::Mailbox;
use futures::TryStreamExt;
use tokio::sync::Mutex;
use crate::imap::capabilities::Capabilities;
use crate::net::session::SessionStream;
use crate::tools;
/// Prefetch:
/// - Message-ID to check if we already have the message.
@@ -46,8 +44,6 @@ pub(crate) struct Session {
pub selected_folder_needs_expunge: bool,
pub(crate) last_full_folder_scan: Mutex<Option<tools::Time>>,
/// True if currently selected folder has new messages.
///
/// Should be false if no folder is currently selected.
@@ -84,7 +80,6 @@ impl Session {
selected_folder: None,
selected_mailbox: None,
selected_folder_needs_expunge: false,
last_full_folder_scan: Mutex::new(None),
new_mail: false,
resync_request_sender,
}
+3 -3
View File
@@ -19,7 +19,7 @@ use crate::config::Config;
use crate::context::Context;
use crate::e2ee;
use crate::events::EventType;
use crate::key::{self, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey};
use crate::key::{self, DcKey, SignedPublicKey, SignedSecretKey};
use crate::log::{LogExt, warn};
use crate::pgp;
use crate::qr::DCBACKUP_VERSION;
@@ -142,7 +142,7 @@ pub async fn has_backup(_context: &Context, dir_name: &Path) -> Result<String> {
async fn set_self_key(context: &Context, armored: &str) -> Result<()> {
let private_key = SignedSecretKey::from_asc(armored)?;
let public_key = private_key.split_public_key()?;
let public_key = private_key.to_public_key();
let keypair = pgp::KeyPair {
public: public_key,
@@ -153,7 +153,7 @@ async fn set_self_key(context: &Context, armored: &str) -> Result<()> {
info!(
context,
"stored self key: {:?}",
keypair.secret.public_key().key_id()
keypair.secret.public_key().legacy_key_id()
);
Ok(())
}
+4 -33
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::{KeyDetails, KeyId, Password};
use pgp::types::{KeyDetails, KeyId};
use tokio::runtime::Handle;
use crate::context::Context;
@@ -264,7 +264,7 @@ impl DcKey for SignedPublicKey {
}
fn key_id(&self) -> KeyId {
KeyDetails::key_id(self)
KeyDetails::legacy_key_id(self)
}
}
@@ -291,30 +291,7 @@ impl DcKey for SignedSecretKey {
}
fn key_id(&self) -> KeyId {
KeyDetails::key_id(&**self)
}
}
/// Deltachat extension trait for secret keys.
///
/// Provides some convenience wrappers only applicable to [SignedSecretKey].
pub(crate) trait DcSecretKey {
/// Create a public key from a private one.
fn split_public_key(&self) -> Result<SignedPublicKey>;
}
impl DcSecretKey for SignedSecretKey {
fn split_public_key(&self) -> Result<SignedPublicKey> {
self.verify()?;
let unsigned_pubkey = self.public_key();
let mut rng = rand_old::thread_rng();
let signed_pubkey = unsigned_pubkey.sign(
&mut rng,
&self.primary_key,
self.primary_key.public_key(),
&Password::empty(),
)?;
Ok(signed_pubkey)
KeyDetails::legacy_key_id(&**self)
}
}
@@ -426,7 +403,7 @@ pub(crate) async fn store_self_keypair(context: &Context, keypair: &KeyPair) ->
/// Use import/export APIs instead.
pub async fn preconfigure_keypair(context: &Context, secret_data: &str) -> Result<()> {
let secret = SignedSecretKey::from_asc(secret_data)?;
let public = secret.split_public_key()?;
let public = secret.to_public_key();
let keypair = KeyPair { public, secret };
store_self_keypair(context, &keypair).await?;
Ok(())
@@ -702,12 +679,6 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
assert_eq!(res0.unwrap(), res1.unwrap());
}
#[test]
fn test_split_key() {
let pubkey = KEYPAIR.secret.split_public_key().unwrap();
assert_eq!(pubkey.primary_key, KEYPAIR.public.primary_key);
}
/// Tests that setting a default key second time is not allowed.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_save_self_key_twice() {
+2 -1
View File
@@ -14,7 +14,8 @@
clippy::unused_async,
clippy::explicit_iter_loop,
clippy::explicit_into_iter_loop,
clippy::cloned_instead_of_copied
clippy::cloned_instead_of_copied,
clippy::manual_is_variant_and
)]
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
#![cfg_attr(not(test), forbid(clippy::string_slice))]
+4 -5
View File
@@ -980,7 +980,7 @@ impl Message {
/// Returns true if the message is a forwarded message.
pub fn is_forwarded(&self) -> bool {
0 != self.param.get_int(Param::Forwarded).unwrap_or_default()
self.param.get_int(Param::Forwarded).is_some()
}
/// Returns true if the message is edited.
@@ -1762,12 +1762,11 @@ pub async fn delete_msgs_ex(
modified_chat_ids.insert(msg.chat_id);
deleted_rfc724_mid.push(msg.rfc724_mid.clone());
let target = context.get_delete_msgs_target().await?;
let update_db = |trans: &mut rusqlite::Transaction| {
let mut stmt = trans.prepare("UPDATE imap SET target=? WHERE rfc724_mid=?")?;
stmt.execute((&target, &msg.rfc724_mid))?;
let mut stmt = trans.prepare("UPDATE imap SET target='' WHERE rfc724_mid=?")?;
stmt.execute((&msg.rfc724_mid,))?;
if !msg.pre_rfc724_mid.is_empty() {
stmt.execute((&target, &msg.pre_rfc724_mid))?;
stmt.execute((&msg.pre_rfc724_mid,))?;
}
trans.execute("DELETE FROM smtp WHERE msg_id=?", (msg_id,))?;
trans.execute(
+14 -4
View File
@@ -1840,6 +1840,12 @@ impl MimeFactory {
mail_builder::headers::raw::Raw::new(b_encode(answer)).into(),
));
}
if let Some(has_video) = msg.param.get(Param::WebrtcHasVideoInitially) {
headers.push((
"Chat-Webrtc-Has-Video-Initially",
mail_builder::headers::raw::Raw::new(b_encode(has_video)).into(),
))
}
if msg.viewtype == Viewtype::Voice
|| msg.viewtype == Viewtype::Audio
@@ -1934,15 +1940,19 @@ impl MimeFactory {
let mut parts = Vec::new();
// add HTML-part, this is needed only if a HTML-message from a non-delta-client is forwarded;
// for simplificity and to avoid conversion errors, we're generating the HTML-part from the original message.
if msg.has_html() {
let html = if let Some(orig_msg_id) = msg.param.get_int(Param::Forwarded) {
let html = if let Some(html) = msg.param.get(Param::SendHtml) {
Some(html.to_string())
} else if let Some(orig_msg_id) = msg.param.get_int(Param::Forwarded)
&& orig_msg_id != 0
{
// Legacy forwarded messages may not have `Param::SendHtml` set. Let's hope the
// original message exists.
MsgId::new(orig_msg_id.try_into()?)
.get_html(context)
.await?
} else {
msg.param.get(Param::SendHtml).map(|s| s.to_string())
None
};
if let Some(html) = html {
main_part = MimePart::new(
+8 -3
View File
@@ -496,8 +496,7 @@ impl MimeMessage {
// We don't decompress messages compressed multiple times.
None
}
Some(pgp::composed::Message::SignedOnePass { reader, .. }) => reader.signature(),
Some(pgp::composed::Message::Signed { reader, .. }) => Some(reader.signature()),
Some(pgp::composed::Message::Signed { reader, .. }) => reader.signature(0),
Some(pgp::composed::Message::Encrypted { .. }) => {
// The message is already decrypted once.
None
@@ -801,6 +800,9 @@ impl MimeMessage {
let accepted = self
.get_header(HeaderDef::ChatWebrtcAccepted)
.map(|s| s.to_string());
let has_video = self
.get_header(HeaderDef::ChatWebrtcHasVideoInitially)
.map(|s| s.to_string());
if let Some(part) = self.parts.first_mut() {
if let Some(room) = room {
if content == "call" {
@@ -810,6 +812,9 @@ impl MimeMessage {
} else if let Some(accepted) = accepted {
part.param.set(Param::WebrtcAccepted, accepted);
}
if let Some(has_video) = has_video {
part.param.set(Param::WebrtcHasVideoInitially, has_video);
}
}
}
@@ -1630,7 +1635,7 @@ impl MimeMessage {
}
Ok(key) => key,
};
if let Err(err) = key.verify() {
if let Err(err) = key.verify_bindings() {
warn!(context, "Attached PGP key verification failed: {err:#}.");
return Ok(false);
}
+3
View File
@@ -148,6 +148,9 @@ pub enum Param {
/// For Messages
WebrtcAccepted = b'7',
/// For Messages
WebrtcHasVideoInitially = b'z',
/// For Messages: space-separated list of messaged IDs of forwarded copies.
///
/// This is used when a [crate::message::Message] is in the
+18 -24
View File
@@ -4,13 +4,12 @@ use std::collections::{BTreeMap, HashMap, HashSet};
use std::io::{BufRead, Cursor};
use anyhow::{Context as _, Result, bail};
use chrono::SubsecRound;
use deltachat_contact_tools::EmailAddress;
use pgp::armor::BlockType;
use pgp::composed::{
ArmorOptions, DecryptionOptions, Deserializable, DetachedSignature, KeyType as PgpKeyType,
Message, MessageBuilder, SecretKeyParamsBuilder, SignedPublicKey, SignedPublicSubKey,
SignedSecretKey, SubkeyParamsBuilder, SubpacketConfig, TheRing,
ArmorOptions, DecryptionOptions, Deserializable, DetachedSignature, EncryptionCaps,
KeyType as PgpKeyType, Message, MessageBuilder, SecretKeyParamsBuilder, SignedPublicKey,
SignedPublicSubKey, SignedSecretKey, SubkeyParamsBuilder, SubpacketConfig, TheRing,
};
use pgp::crypto::aead::{AeadAlgorithm, ChunkSize};
use pgp::crypto::ecc_curve::ECCCurve;
@@ -18,8 +17,7 @@ use pgp::crypto::hash::HashAlgorithm;
use pgp::crypto::sym::SymmetricKeyAlgorithm;
use pgp::packet::{SignatureConfig, SignatureType, Subpacket, SubpacketData};
use pgp::types::{
CompressionAlgorithm, KeyDetails, KeyVersion, Password, PublicKeyTrait, SecretKeyTrait as _,
StringToKey,
CompressionAlgorithm, KeyDetails, KeyVersion, Password, SigningKey as _, StringToKey,
};
use rand_old::{Rng as _, thread_rng};
use tokio::runtime::Handle;
@@ -83,9 +81,7 @@ impl KeyPair {
///
/// Public key is split off the secret key.
pub fn new(secret: SignedSecretKey) -> Result<Self> {
use crate::key::DcSecretKey;
let public = secret.split_public_key()?;
let public = secret.to_public_key();
Ok(Self { public, secret })
}
}
@@ -123,7 +119,7 @@ pub(crate) fn create_keypair(addr: EmailAddress) -> Result<KeyPair> {
.subkey(
SubkeyParamsBuilder::default()
.key_type(encryption_key_type)
.can_encrypt(true)
.can_encrypt(EncryptionCaps::All)
.passphrase(None)
.build()
.context("failed to build subkey parameters")?,
@@ -134,18 +130,16 @@ pub(crate) fn create_keypair(addr: EmailAddress) -> Result<KeyPair> {
let mut rng = thread_rng();
let secret_key = key_params
.generate(&mut rng)
.context("failed to generate the key")?
.sign(&mut rng, &Password::empty())
.context("failed to sign secret key")?;
.context("Failed to generate the key")?;
secret_key
.verify()
.context("invalid secret key generated")?;
.verify_bindings()
.context("Invalid secret key generated")?;
let key_pair = KeyPair::new(secret_key)?;
key_pair
.public
.verify()
.context("invalid public key generated")?;
.verify_bindings()
.context("Invalid public key generated")?;
Ok(key_pair)
}
@@ -157,7 +151,7 @@ pub(crate) fn create_keypair(addr: EmailAddress) -> Result<KeyPair> {
fn select_pk_for_encryption(key: &SignedPublicKey) -> Option<&SignedPublicSubKey> {
key.public_subkeys
.iter()
.find(|subkey| subkey.is_encryption_key())
.find(|subkey| subkey.algorithm().can_encrypt())
}
/// Version of SEIPD packet to use.
@@ -194,7 +188,7 @@ pub async fn pk_encrypt(
let subpkts = {
let mut hashed = Vec::with_capacity(1 + public_keys_for_encryption.len() + 1);
hashed.push(Subpacket::critical(SubpacketData::SignatureCreationTime(
chrono::Utc::now().trunc_subsecs(0),
pgp::types::Timestamp::now(),
))?);
// Test "elena" uses old Delta Chat.
let skip = private_key_for_signing.dc_fingerprint().hex()
@@ -215,8 +209,8 @@ pub async fn pk_encrypt(
))?);
let mut unhashed = vec![];
if private_key_for_signing.version() <= KeyVersion::V4 {
unhashed.push(Subpacket::regular(SubpacketData::Issuer(
private_key_for_signing.key_id(),
unhashed.push(Subpacket::regular(SubpacketData::IssuerKeyId(
private_key_for_signing.legacy_key_id(),
))?);
}
SubpacketConfig::UserDefined { hashed, unhashed }
@@ -302,15 +296,15 @@ pub fn pk_calc_signature(
private_key_for_signing.fingerprint(),
))?,
Subpacket::critical(SubpacketData::SignatureCreationTime(
chrono::Utc::now().trunc_subsecs(0),
pgp::types::Timestamp::now(),
))?,
];
config.unhashed_subpackets = vec![];
if private_key_for_signing.version() <= KeyVersion::V4 {
config
.unhashed_subpackets
.push(Subpacket::regular(SubpacketData::Issuer(
private_key_for_signing.key_id(),
.push(Subpacket::regular(SubpacketData::IssuerKeyId(
private_key_for_signing.legacy_key_id(),
))?);
}
+1 -4
View File
@@ -145,6 +145,7 @@ pub struct Provider {
/// Provider options with good defaults.
#[derive(Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct ProviderOptions {
/// True if provider is known to use use proper,
/// not self-signed certificates.
@@ -152,9 +153,6 @@ pub struct ProviderOptions {
/// Maximum number of recipients the provider allows to send a single email to.
pub max_smtp_rcpt_to: Option<u16>,
/// Move messages to the Trash folder instead of marking them "\Deleted".
pub delete_to_trash: bool,
}
impl ProviderOptions {
@@ -162,7 +160,6 @@ impl ProviderOptions {
Self {
strict_tls: true,
max_smtp_rcpt_to: None,
delete_to_trash: false,
}
}
}
+2 -5
View File
@@ -577,10 +577,7 @@ static P_GMAIL: Provider = Provider {
username_pattern: Email,
},
],
opt: ProviderOptions {
delete_to_trash: true,
..ProviderOptions::new()
},
opt: ProviderOptions::new(),
config_defaults: None,
oauth2_authorizer: None,
};
@@ -2625,4 +2622,4 @@ pub(crate) static PROVIDER_IDS: LazyLock<HashMap<&'static str, &'static Provider
});
pub static _PROVIDER_UPDATED: LazyLock<chrono::NaiveDate> =
LazyLock::new(|| chrono::NaiveDate::from_ymd_opt(2026, 1, 27).unwrap());
LazyLock::new(|| chrono::NaiveDate::from_ymd_opt(2026, 1, 28).unwrap());
+1 -1
View File
@@ -9,7 +9,7 @@ use async_imap::types::{Quota, QuotaResource};
use crate::chat::add_device_msg_with_importance;
use crate::config::Config;
use crate::context::Context;
use crate::imap::scan_folders::get_watched_folders;
use crate::imap::get_watched_folders;
use crate::imap::session::Session as ImapSession;
use crate::log::warn;
use crate::message::Message;
+1 -1
View File
@@ -952,7 +952,7 @@ UPDATE config SET value=? WHERE keyname='configured_addr' AND value!=?1
if !received_msg.msg_ids.is_empty() {
let target = if received_msg.needs_delete_job || delete_server_after == Some(0) {
Some(context.get_delete_msgs_target().await?)
Some("".to_string())
} else {
None
};
+16 -71
View File
@@ -256,14 +256,6 @@ impl SchedulerState {
}
}
/// Interrupt optional boxes (mvbox currently) loops.
pub(crate) async fn interrupt_oboxes(&self) {
let inner = self.inner.read().await;
if let InnerSchedulerState::Started(ref scheduler) = *inner {
scheduler.interrupt_oboxes();
}
}
pub(crate) async fn interrupt_smtp(&self) {
let inner = self.inner.read().await;
if let InnerSchedulerState::Started(ref scheduler) = *inner {
@@ -519,65 +511,24 @@ async fn fetch_idle(
.context("store_seen_flags_on_imap")?;
}
if !ctx.should_delete_to_trash().await?
|| ctx
.get_config(Config::ConfiguredTrashFolder)
.await?
.is_some()
{
// Fetch the watched folder.
connection
.fetch_move_delete(ctx, &mut session, &watch_folder, folder_meaning)
.await
.context("fetch_move_delete")?;
// Fetch the watched folder.
connection
.fetch_move_delete(ctx, &mut session, &watch_folder, folder_meaning)
.await
.context("fetch_move_delete")?;
// Mark expired messages for deletion. Marked messages will be deleted from the server
// on the next iteration of `fetch_move_delete`. `delete_expired_imap_messages` is not
// called right before `fetch_move_delete` because it is not well optimized and would
// otherwise slow down message fetching.
delete_expired_imap_messages(ctx)
.await
.context("delete_expired_imap_messages")?;
// Mark expired messages for deletion. Marked messages will be deleted from the server
// on the next iteration of `fetch_move_delete`. `delete_expired_imap_messages` is not
// called right before `fetch_move_delete` because it is not well optimized and would
// otherwise slow down message fetching.
delete_expired_imap_messages(ctx)
.await
.context("delete_expired_imap_messages")?;
download_known_post_messages_without_pre_message(ctx, &mut session).await?;
download_msgs(ctx, &mut session)
.await
.context("download_msgs")?;
} else if folder_config == Config::ConfiguredInboxFolder {
session.last_full_folder_scan.lock().await.take();
}
// Scan additional folders only after finishing fetching the watched folder.
//
// On iOS the application has strictly limited time to work in background, so we may not
// be able to scan all folders before time is up if there are many of them.
if folder_config == Config::ConfiguredInboxFolder {
// Only scan on the Inbox thread in order to prevent parallel scans, which might lead to duplicate messages
match connection
.scan_folders(ctx, &mut session)
.await
.context("scan_folders")
{
Err(err) => {
// Don't reconnect, if there is a problem with the connection we will realize this when IDLEing
// but maybe just one folder can't be selected or something
warn!(ctx, "{:#}", err);
}
Ok(true) => {
// Fetch the watched folder again in case scanning other folder moved messages
// there.
//
// In most cases this will select the watched folder and return because there are
// no new messages. We want to select the watched folder anyway before going IDLE
// there, so this does not take additional protocol round-trip.
connection
.fetch_move_delete(ctx, &mut session, &watch_folder, folder_meaning)
.await
.context("fetch_move_delete after scan_folders")?;
}
Ok(false) => {}
}
}
download_known_post_messages_without_pre_message(ctx, &mut session).await?;
download_msgs(ctx, &mut session)
.await
.context("download_msgs")?;
// Synchronize Seen flags.
session
@@ -902,12 +853,6 @@ impl Scheduler {
}
}
fn interrupt_oboxes(&self) {
for b in &self.oboxes {
b.conn_state.interrupt();
}
}
fn interrupt_smtp(&self) {
self.smtp.interrupt();
}
+1 -1
View File
@@ -6,7 +6,7 @@ use anyhow::Result;
use humansize::{BINARY, format_size};
use crate::events::EventType;
use crate::imap::{FolderMeaning, scan_folders::get_watched_folder_configs};
use crate::imap::{FolderMeaning, get_watched_folder_configs};
use crate::quota::{QUOTA_ERROR_THRESHOLD_PERCENTAGE, QUOTA_WARN_THRESHOLD_PERCENTAGE};
use crate::stock_str;
use crate::{context::Context, log::LogExt};
+21
View File
@@ -9,6 +9,7 @@ use rusqlite::{Connection, OpenFlags, Row, config::DbConfig, types::ValueRef};
use tokio::sync::RwLock;
use crate::blob::BlobObject;
use crate::chat::add_device_msg;
use crate::config::Config;
use crate::constants::DC_CHAT_ID_TRASH;
use crate::context::Context;
@@ -17,11 +18,13 @@ use crate::ephemeral::start_ephemeral_timers;
use crate::imex::BLOBS_BACKUP_NAME;
use crate::location::delete_orphaned_poi_locations;
use crate::log::{LogExt, warn};
use crate::message::Message;
use crate::message::MsgId;
use crate::net::dns::prune_dns_cache;
use crate::net::http::http_cache_cleanup;
use crate::net::prune_connection_history;
use crate::param::{Param, Params};
use crate::stock_str;
use crate::tools::{SystemTime, Time, delete_file, time, time_elapsed};
/// Extension to [`rusqlite::ToSql`] trait
@@ -865,6 +868,12 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
);
}
maybe_add_mvbox_move_deprecation_message(context)
.await
.context("maybe_add_mvbox_move_deprecation_message")
.log_err(context)
.ok();
if let Err(err) = incremental_vacuum(context).await {
warn!(context, "Failed to run incremental vacuum: {err:#}.");
}
@@ -924,6 +933,18 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
Ok(())
}
/// Adds device message about `mvbox_move` config deprecation
/// if the user has it enabled.
async fn maybe_add_mvbox_move_deprecation_message(context: &Context) -> Result<()> {
if !context.get_config_bool(Config::OnlyFetchMvbox).await?
&& context.get_config_bool(Config::MvboxMove).await?
{
let mut msg = Message::new_text(stock_str::mvbox_move_deprecation(context).await);
add_device_msg(context, Some("mvbox_move_deprecation"), Some(&mut msg)).await?;
}
Ok(())
}
/// Get the value of a column `idx` of the `row` as `Vec<u8>`.
pub fn row_get_vec(row: &Row, idx: usize) -> rusqlite::Result<Vec<u8>> {
row.get(idx).or_else(|err| match row.get_ref(idx)? {
+33
View File
@@ -1525,6 +1525,39 @@ ALTER TABLE contacts ADD COLUMN name_normalized TEXT;
.await?;
}
// Copy of migration 144, but inserts a value for `mvbox_move`
// if it does not exist, because no value is treated as `1`.
inc_and_check(&mut migration_version, 146)?;
if dbversion < migration_version {
sql.execute_migration_transaction(
|transaction| {
let is_chatmail = transaction
.query_row(
"SELECT value FROM config WHERE keyname='is_chatmail'",
(),
|row| {
let value: String = row.get(0)?;
Ok(value)
},
)
.optional()?
.as_deref()
== Some("1");
if is_chatmail {
transaction.execute_batch(
"DELETE FROM config WHERE keyname='only_fetch_mvbox';
DELETE FROM config WHERE keyname='show_emails';
INSERT OR REPLACE INTO config (keyname, value) VALUES ('mvbox_move', '0')",
)?;
}
Ok(())
},
migration_version,
)
.await?;
}
let new_version = sql
.get_raw_config_int(VERSION_CFG)
.await?
+4 -4
View File
@@ -8,7 +8,7 @@ use std::collections::{BTreeMap, BTreeSet};
use anyhow::{Context as _, Result};
use deltachat_derive::FromSql;
use num_traits::ToPrimitive;
use pgp::types::PublicKeyTrait;
use pgp::types::KeyDetails as _;
use rand::distr::SampleString as _;
use rusqlite::OptionalExtension;
use serde::Serialize;
@@ -33,7 +33,7 @@ const MESSAGE_STATS_UPDATE_INTERVAL_SECONDS: i64 = 4 * 60; // 4 minutes (less th
#[derive(Serialize)]
struct Statistics {
core_version: String,
key_create_timestamps: Vec<i64>,
key_create_timestamps: Vec<u32>,
stats_id: String,
is_chatmail: bool,
contact_stats: Vec<ContactStat>,
@@ -345,10 +345,10 @@ async fn get_stats(context: &Context) -> Result<String> {
.get_config_u32(Config::StatsLastOldContactId)
.await?;
let key_create_timestamps: Vec<i64> = load_self_public_keyring(context)
let key_create_timestamps: Vec<u32> = load_self_public_keyring(context)
.await?
.iter()
.map(|k| k.created_at().timestamp())
.map(|k| k.created_at().as_secs())
.collect();
let sending_enabled_timestamps =
+44 -12
View File
@@ -369,12 +369,6 @@ Help keeping us to keep Delta Chat independent and make it more awesome in the f
https://delta.chat/donate"))]
DonationRequest = 193,
#[strum(props(fallback = "Outgoing call"))]
OutgoingCall = 194,
#[strum(props(fallback = "Incoming call"))]
IncomingCall = 195,
#[strum(props(fallback = "Declined call"))]
DeclinedCall = 196,
@@ -412,6 +406,23 @@ https://delta.chat/donate"))]
#[strum(props(fallback = "Messages in this chat use classic email and are not encrypted."))]
ChatUnencryptedExplanation = 230,
#[strum(props(
fallback = "You are using the legacy option \"Settings → Advanced → Move automatically to DeltaChat Folder\".\n\nThis option will be removed in a few weeks and you should disable it already today.\n\nIf having chat messages mixed into your inbox is a problem, see https://delta.chat/legacy-move"
))]
MvboxMoveDeprecation = 231,
#[strum(props(fallback = "Outgoing audio call"))]
OutgoingAudioCall = 232,
#[strum(props(fallback = "Outgoing video call"))]
OutgoingVideoCall = 233,
#[strum(props(fallback = "Incoming audio call"))]
IncomingAudioCall = 234,
#[strum(props(fallback = "Incoming video call"))]
IncomingVideoCall = 235,
}
impl StockMessage {
@@ -757,14 +768,30 @@ pub(crate) async fn donation_request(context: &Context) -> String {
translated(context, StockMessage::DonationRequest).await
}
/// Stock string: `Outgoing call`.
pub(crate) async fn outgoing_call(context: &Context) -> String {
translated(context, StockMessage::OutgoingCall).await
/// Stock string: `Outgoing video call` or `Outgoing audio call`.
pub(crate) async fn outgoing_call(context: &Context, has_video: bool) -> String {
translated(
context,
if has_video {
StockMessage::OutgoingVideoCall
} else {
StockMessage::OutgoingAudioCall
},
)
.await
}
/// Stock string: `Incoming call`.
pub(crate) async fn incoming_call(context: &Context) -> String {
translated(context, StockMessage::IncomingCall).await
/// Stock string: `Incoming video call` or `Incoming audio call`.
pub(crate) async fn incoming_call(context: &Context, has_video: bool) -> String {
translated(
context,
if has_video {
StockMessage::IncomingVideoCall
} else {
StockMessage::IncomingAudioCall
},
)
.await
}
/// Stock string: `Declined call`.
@@ -1223,6 +1250,11 @@ pub(crate) async fn chat_unencrypted_explanation(context: &Context) -> String {
translated(context, StockMessage::ChatUnencryptedExplanation).await
}
/// Stock string: `You are using the legacy option "Move automatically to DeltaChat Folder`…
pub(crate) async fn mvbox_move_deprecation(context: &Context) -> String {
translated(context, StockMessage::MvboxMoveDeprecation).await
}
impl Viewtype {
/// returns Localized name for message viewtype
pub async fn to_locale_string(&self, context: &Context) -> String {
+5 -3
View File
@@ -247,16 +247,18 @@ impl Message {
append_text = true;
}
Viewtype::Call => {
let call_info = context.load_call_by_id(self.id).await.unwrap_or(None);
let has_video = call_info.is_some_and(|c| c.has_video_initially());
let call_state = call_state(context, self.id)
.await
.unwrap_or(CallState::Alerting);
emoji = Some("📞");
emoji = Some(if has_video { "🎥" } else { "📞" });
type_name = Some(match call_state {
CallState::Alerting | CallState::Active | CallState::Completed { .. } => {
if self.from_id == ContactId::SELF {
stock_str::outgoing_call(context).await
stock_str::outgoing_call(context, has_video).await
} else {
stock_str::incoming_call(context).await
stock_str::incoming_call(context, has_video).await
}
}
CallState::Missed => stock_str::missed_call(context).await,
+8 -7
View File
@@ -32,7 +32,7 @@ use crate::contact::{
};
use crate::context::Context;
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::key::{self, DcKey, DcSecretKey, self_fingerprint};
use crate::key::{self, DcKey, self_fingerprint};
use crate::log::warn;
use crate::login_param::EnteredLoginParam;
use crate::message::{Message, MessageState, MsgId, update_msg_state};
@@ -567,6 +567,7 @@ impl TestContext {
.unwrap();
ctx.set_config(Config::BccSelf, Some("1")).await.unwrap();
ctx.set_config(Config::SyncMsgs, Some("0")).await.unwrap();
ctx.set_config(Config::MvboxMove, Some("0")).await.unwrap();
Self {
ctx,
@@ -1354,7 +1355,7 @@ impl SentMessage<'_> {
pub fn alice_keypair() -> KeyPair {
let secret =
key::SignedSecretKey::from_asc(include_str!("../test-data/key/alice-secret.asc")).unwrap();
let public = secret.split_public_key().unwrap();
let public = secret.to_public_key();
KeyPair { public, secret }
}
@@ -1364,7 +1365,7 @@ pub fn alice_keypair() -> KeyPair {
pub fn bob_keypair() -> KeyPair {
let secret =
key::SignedSecretKey::from_asc(include_str!("../test-data/key/bob-secret.asc")).unwrap();
let public = secret.split_public_key().unwrap();
let public = secret.to_public_key();
KeyPair { public, secret }
}
@@ -1375,7 +1376,7 @@ pub fn charlie_keypair() -> KeyPair {
let secret =
key::SignedSecretKey::from_asc(include_str!("../test-data/key/charlie-secret.asc"))
.unwrap();
let public = secret.split_public_key().unwrap();
let public = secret.to_public_key();
KeyPair { public, secret }
}
@@ -1385,7 +1386,7 @@ pub fn charlie_keypair() -> KeyPair {
pub fn dom_keypair() -> KeyPair {
let secret =
key::SignedSecretKey::from_asc(include_str!("../test-data/key/dom-secret.asc")).unwrap();
let public = secret.split_public_key().unwrap();
let public = secret.to_public_key();
KeyPair { public, secret }
}
@@ -1395,7 +1396,7 @@ pub fn dom_keypair() -> KeyPair {
pub fn elena_keypair() -> KeyPair {
let secret =
key::SignedSecretKey::from_asc(include_str!("../test-data/key/elena-secret.asc")).unwrap();
let public = secret.split_public_key().unwrap();
let public = secret.to_public_key();
KeyPair { public, secret }
}
@@ -1405,7 +1406,7 @@ pub fn elena_keypair() -> KeyPair {
pub fn fiona_keypair() -> KeyPair {
let secret =
key::SignedSecretKey::from_asc(include_str!("../test-data/key/fiona-secret.asc")).unwrap();
let public = secret.split_public_key().unwrap();
let public = secret.to_public_key();
KeyPair { public, secret }
}