Compare commits

..

84 Commits

Author SHA1 Message Date
link2xt
8d6f4b0354 chore(release): prepare for 2.36.0 2026-01-03 18:39:16 +00:00
link2xt
284469363e docs: remove references to sentbox_watch config
This config was already removed in 2.23.0.
2026-01-03 13:40:24 +00:00
dependabot[bot]
6078c79020 chore(cargo): bump criterion from 0.7.0 to 0.8.1
Bumps [criterion](https://github.com/criterion-rs/criterion.rs) from 0.7.0 to 0.8.1.
- [Release notes](https://github.com/criterion-rs/criterion.rs/releases)
- [Changelog](https://github.com/criterion-rs/criterion.rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/criterion-rs/criterion.rs/compare/criterion-plot-v0.7.0...criterion-v0.8.1)

---
updated-dependencies:
- dependency-name: criterion
  dependency-version: 0.8.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...
2026-01-02 15:50:49 -03:00
dependabot[bot]
161e5ae358 chore(cargo): bump rustls-pki-types from 1.13.0 to 1.13.2
Bumps [rustls-pki-types](https://github.com/rustls/pki-types) from 1.13.0 to 1.13.2.
- [Release notes](https://github.com/rustls/pki-types/releases)
- [Commits](https://github.com/rustls/pki-types/compare/v/1.13.0...v/1.13.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-02 15:32:06 -03:00
dependabot[bot]
a66859ebf2 chore(cargo): bump log from 0.4.28 to 0.4.29
Bumps [log](https://github.com/rust-lang/log) from 0.4.28 to 0.4.29.
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.28...0.4.29)

---
updated-dependencies:
- dependency-name: log
  dependency-version: 0.4.29
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-02 15:29:04 -03:00
dependabot[bot]
de902babc3 chore(cargo): bump hyper-util from 0.1.18 to 0.1.19
Bumps [hyper-util](https://github.com/hyperium/hyper-util) from 0.1.18 to 0.1.19.
- [Release notes](https://github.com/hyperium/hyper-util/releases)
- [Changelog](https://github.com/hyperium/hyper-util/blob/master/CHANGELOG.md)
- [Commits](https://github.com/hyperium/hyper-util/compare/v0.1.18...v0.1.19)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-02 14:48:44 -03:00
dependabot[bot]
98a8679779 chore(cargo): bump tracing from 0.1.41 to 0.1.44
Bumps [tracing](https://github.com/tokio-rs/tracing) from 0.1.41 to 0.1.44.
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-0.1.41...tracing-0.1.44)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-02 14:46:33 -03:00
dependabot[bot]
8ce3ecc809 chore(cargo): bump libc from 0.2.177 to 0.2.178
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.177 to 0.2.178.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.178/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.177...0.2.178)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-02 14:39:44 -03:00
dependabot[bot]
50a1f907a5 chore(cargo): bump tempfile from 3.23.0 to 3.24.0
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.23.0 to 3.24.0.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.23.0...v3.24.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-02 14:18:29 -03:00
dependabot[bot]
c2bf2a32b5 chore(cargo): bump toml from 0.9.8 to 0.9.10+spec-1.1.0
Bumps [toml](https://github.com/toml-rs/toml) from 0.9.8 to 0.9.10+spec-1.1.0.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.9.8...toml-v0.9.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-02 04:41:37 -03:00
dependabot[bot]
84710384fe chore(cargo): bump uuid from 1.18.1 to 1.19.0
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.18.1 to 1.19.0.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.18.1...v1.19.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-02 04:38:57 -03:00
dependabot[bot]
f60fce22ed chore(cargo): bump serde_json from 1.0.145 to 1.0.147
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.145 to 1.0.147.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.145...v1.0.147)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-02 04:36:56 -03:00
link2xt
0d2f2b3266 refactor(ffi): remove one pointer indirection for dc_accounts_t
Arc can be converted directly to raw pointers,
so there is no need to create a pointer to Arc
using a Box.
2026-01-01 15:34:55 +00:00
Casper Zandbergen
516f0a1a98 fix: Don't send webxdc notification for notify: "*" when chat is muted (#7658)
- webxdc notify specifically to Bob: notifies Bob even when chat is
muted
- webxdc notify to everyone ("*"): notifies Bob if he does not have the
chat muted

This aligns with how we handle notifications with quote replies.

---------

Co-authored-by: link2xt <link2xt@testrun.org>
2025-12-30 23:14:35 +01:00
link2xt
25750de4e1 feat: send sync messages over SMTP and do not move them to mvbox 2025-12-26 10:58:33 +00:00
link2xt
a89ce8ce7a docs: update documentation for TransportsModified event
It is a follow-up to https://github.com/chatmail/core/pull/7643
Event is not emitted when the transports are modified on this device
and we should consistently say that this event is not only for testing.
2025-12-25 11:16:36 +00:00
Simon Laux
9ac64ea6b9 feat: connectivity view: quota for all transports (#7630)
- **show quota of all relays**
- **remove `DC_STR_STORAGE_ON_DOMAIN` stock string**
- renames the quota section to "Relay Capacity" until we come up with a
better name in
https://github.com/chatmail/core/issues/7580#issuecomment-3633803432

closes #7591

<img width="300" alt="image"
src="https://github.com/user-attachments/assets/1909dccd-e6b3-42e6-963f-004b2b464db7"
/> <img width="300" alt="image"
src="https://github.com/user-attachments/assets/1e97e67b-e0ed-492b-95a0-6ef12595abe4"
/>
2025-12-24 12:19:58 +00:00
iequidoo
294e23d82d docs: delete_chat(): Don't lie that messages aren't deleted from server
Messages are actually deleted from the server. I've checked this in Desktop.
2025-12-24 00:56:41 -03:00
link2xt
184736723f fix: reset options not available for chatmail on chatmail profiles 2025-12-24 03:23:23 +00:00
dependabot[bot]
cea528ed61 chore(deps): bump cachix/install-nix-action from 31.8.4 to 31.9.0
Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 31.8.4 to 31.9.0.
- [Release notes](https://github.com/cachix/install-nix-action/releases)
- [Changelog](https://github.com/cachix/install-nix-action/blob/master/RELEASE.md)
- [Commits](0b0e072294...4e002c8ec8)

---
updated-dependencies:
- dependency-name: cachix/install-nix-action
  dependency-version: 31.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-23 21:09:35 +00:00
dependabot[bot]
9b11f53da6 chore(deps): bump astral-sh/setup-uv from 7.1.4 to 7.1.6
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 7.1.4 to 7.1.6.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](1e862dfacb...681c641aba)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 7.1.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-23 21:09:01 +00:00
B. Petersen
5c339efb70 feat: add transports event to ffi 2025-12-23 12:04:32 +01:00
dependabot[bot]
d71c163c7d chore(deps): bump actions/upload-artifact from 5 to 6
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-23 08:00:54 +00:00
dependabot[bot]
19fde9594f chore(deps): bump actions/download-artifact from 6 to 7
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-23 08:00:33 +00:00
iequidoo
20b3a06adf fix: inner_configure: Check Config::OnlyFetchMvbox before MvboxMove for multi-transport (#7637)
`Config::OnlyFetchMvbox` should be checked before `MvboxMove` because the latter makes no sense in
presense of `OnlyFetchMvbox` and even grayed out in the UIs in this case. Otherwise users will see
an error mentioning the wrong setting.
2025-12-23 01:22:11 -03:00
B. Petersen
b0127fa381 fix: update fallback welcome message
this sets the welcome message to the one used by the current translations;
the fallback is used if that is not done for whatever reason.
2025-12-21 14:38:31 +01:00
iequidoo
6a293aebe2 test: Port test_import_export_online_all to JSON-RPC (#7411) 2025-12-19 01:17:59 -03:00
link2xt
fd90493766 feat: add core version to receive_imf failure message 2025-12-18 14:44:49 +00:00
link2xt
b1883c802b refactor: turn DC_VERSION_STR into &str 2025-12-18 14:44:49 +00:00
link2xt
71ee32b8b7 ci: pin GitHub Action references
Makes `zizmor` happy.
2025-12-18 14:43:27 +00:00
iequidoo
84161f4202 fix: When accepting group, add members with Origin::IncomingTo and sort them down in the contact list (7592)
When accepting a chat, its members are promoted to `Origin::CreateChat`, but for groups it makes
sense to use lower origin because users don't always check all members before accepting a chat and
may not want to have the group members mixed with existing contacts. `IncomingTo` fits here by its
definition: "additional To:'s of incoming message of known sender", i.e. we assume that the sender
of some message is known to the user. This way we can show contacts coming from groups in the bottom
of contact list, maybe even add some separator later. It makes sense not to hide such contacts
completely, otherwise if the user remembers the contact name, but not the chat it's a member of, it
would be difficult to find the contact.
2025-12-16 21:47:05 -03:00
iequidoo
4af9463a91 test: Contact list after accepting group with unknown contacts (#7592)
This records the curent behavior: after accepting a group with unknown contacts the contact list
contains them mixed with contacts for which 1:1 chats exist, i.e. new contacts from the group are
neither hidden nor sorted down.
2025-12-16 21:47:05 -03:00
link2xt
ddd4fc49a2 chore(release): prepare for 2.35.0 2025-12-16 22:22:20 +00:00
link2xt
7d5bedde4d fix: take transport_id into account when using imap table 2025-12-16 22:08:33 +00:00
iequidoo
e34fee72a0 feat: lookup_host_with_cache(): Don't return empty address list (#7596)
All users of this function expect a nonempty list to be returned, otherwise they fail with hard to
understand errors like "No connection attempts were made", so it's better to fail early if DNS
resolution failed and the cache doesn't contain resolutions as well.
2025-12-16 16:03:17 -03:00
link2xt
7ba4a43253 feat: add transport addresses to IMAP URLs in message info 2025-12-16 16:49:49 +00:00
Simon Laux
a09fd4577a fix: add explicit limit for adding relays (5 at the moment) (#7611)
closes https://github.com/chatmail/core/issues/7608
2025-12-15 10:35:23 +00:00
Simon Laux
525a3539d2 misc: log entered login params and actual used params on configuration failure (#7610)
#7587 removed "used_account_settings" and "entered_account_settings"
from Context.get_info(). link2xt pointed out that
entered_account_settings can still be useful to debug login issues, so
tis pr adds logs both on failed configuration attempts.

example warning event:
```
Warning src/configure.rs:292: configure failed: entered params myself@merlinux.eu imap:unset:***:unset:0:Automatic:AUTH_NORMAL smtp:unset:0:unset:0:Automatic:AUTH_NORMAL cert_automatic, used params myself@merlinux.eu imap:[mailcow.testrun.org:993:tls:myself@merlinux.eu, mailcow.testrun.org:143:starttls:myself@merlinux.eu] smtp:[mailcow.testrun.org:465:tls:myself@merlinux.eu, mailcow.testrun.org:587:starttls:myself@merlinux.eu] provider:none cert_automatic
```

---------

Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com>
2025-12-15 08:37:45 +00:00
Simon Laux
fbcdd45015 feat: add ip addresses of known public chatmail relays from https://chatmail.at/relays to dns cache (#7607)
closes #7597
2025-12-15 08:29:24 +00:00
iequidoo
1ea8ed6442 fix: Use fallback ICE servers if server can't IMAP METADATA (#7382) 2025-12-14 19:44:11 +00:00
iequidoo
f6817131b8 fix: Don't use fallback servers if got TURN servers from IMAP METADATA 2025-12-14 19:44:10 +00:00
iequidoo
28fc1d2ff2 feat: Use turn.delta.chat as fallback TURN server (#7382) 2025-12-14 19:44:10 +00:00
Simon Laux
5925f72316 fix: remove now redundant "used_account_settings" and "entered_account_settings" from Context.get_info() (#7587)
follow up to https://github.com/chatmail/core/pull/7583
2025-12-13 21:21:55 +00:00
Simon Laux
8dfa5fc37e api: add blob dir size to storage info (#7605)
closes #7598
2025-12-12 20:15:12 +00:00
B. Petersen
49b04e8789 feat: improve error messages on adding relays 2025-12-12 18:28:14 +01:00
link2xt
d87d87f467 fix: do not set normalized name for existing chats and contacts in a migration
We got a report that application is not responding after update
on Android and getting killed before it can start,
suspected to be a slow SQL migration:
<https://github.com/chatmail/core/issues/7602>

This change removes calculation of normalized names for
existing chats and contacts added in
<https://github.com/chatmail/core/pull/7548>
to exclude the possibility of this migration being slow.
New chats and contacts will still get normalized names
and all chats and contacts will get it when they are renamed.
2025-12-12 15:44:51 +00:00
iequidoo
bf72b3ad49 fix: Remove SecurejoinWait info message when received Alice's key (#7585)
And don't add a `SecurejoinWait` info message at all if we know Alice's key from the start. If we
don't remove this info message, it appears in the chat after "Messages are end-to-end encrypted..."
which is quite confusing when Bob can already send messages to Alice.
2025-12-12 04:01:32 -03:00
iequidoo
30f2981259 fix: get_chat_msgs_ex(): Don't match on "S=" (Cmd) in param payload 2025-12-12 04:01:32 -03:00
link2xt
121bfd1fa8 ci: update Rust to 1.92.0 2025-12-11 21:23:53 +00:00
link2xt
9e2a4325e9 chore: apply Rust 1.92.0 clippy suggestions 2025-12-11 21:23:53 +00:00
link2xt
4509c1bd06 chore: prepare 2.34.0 release 2025-12-11 16:28:55 +00:00
Hocuri
3133d89dcc fix: Let securejoin succeed even if the chat was deleted in the meantime (#7594)
Fix https://github.com/chatmail/core/issues/7478 by creating the 1:1
chat in `handle_auth_required` if it doesn't exist anymore.
2025-12-11 17:20:41 +01:00
link2xt
99775458c4 test: test setting up second device between core versions 2025-12-10 22:51:17 +00:00
Hocuri
e432960246 feat: Better logging for failing securejoin messages (#7593)
This will make bugs like https://github.com/chatmail/core/issues/7478
easier to debug (even though we may be able to fix #7478 without waiting
for more logs)
2025-12-10 22:08:37 +00:00
link2xt
58cd133b5c fix: synchronize primary transport immediately after changing it 2025-12-09 21:43:26 +00:00
B. Petersen
3d234e7fc7 feat: double ringing time to 120 seconds
the ringing of 60 seconds is indeed a bit short,
esp. when messages are already delayed.
but also if everythings is fast and working, 60 seconds are short.

the 60 seconds also come from the first implementation,
where we did not had a "reject" message from callee to caller,
so that caller should not wait unnecessarily.

this has changed,
however, the other constraints are still valid -
phone may get offline, and we should not ring for stale calls.

sure, that can be fixed also differently,
but for the current implementation,
the 120 seconds seem to be a good compromise.
2025-12-09 15:20:05 +01:00
Simon Laux
595258ae05 fix add multi-transport information to Context.get_info (#7583)
This adds information of all used transports to `Context.get_info` in
the key `used_transport_settings`.

The format is the same as in `used_account_settings`. The new property
also contains the primary account, so we could remove
`used_account_settings` now, though there is also
`entered_account_settings` in which it stays in relation.

- there is an alternative pr at
https://github.com/chatmail/core/pull/7584, which gives each transport
it's own key, which improves readability.

closes #7581
2025-12-09 13:44:55 +00:00
Simon Laux
06b2a890da fix: multi-transport: all transports were shown as "inbox" in connectivity view, now they are shown by their hostname (#7582)
closes #7580
2025-12-09 13:42:24 +00:00
Simon Laux
95ed31391d fix: use logging macros instead of emitting event directly, so that it is also logged by tracing (#7459)
The events are needed when you are not using chatmail core from rust, if
you use chatmail core from your rust bot or from tauri, then you likely
already use the rust logging/tracing ecosystem. So it makes sense to use
it instead of listening to the events and logging them yourself.

This pr fixes a few cases where the event was direclty emitted instead
of using the macro and thus was not also automatically logged via
tracing.
2025-12-09 12:17:31 +00:00
iequidoo
98944efdb8 api: Forwarding messages to another profile (#7491)
Add `chat::forward_msgs_2ctx()` which takes another context as a parameter and forwards messages to
it and its jsonrpc wrapper `CommandApi::forward_messages_to_account()`.
2025-12-09 03:54:54 -03:00
iequidoo
3f27be9bcb refactor: Add params when forwarding message instead of removing unneeded ones
We periodically forget to remove new params from forwarded messages as this can't be catched by
existing tests, some examples:
bfc08abe88
a1837aeb8c
56b2361f01

This may leak confidential data. Instead, it's better to explicitly list params that we want to
forward, then if we forget to forward some param, a test on forwarding messages carrying the new
functionality will break, or the bug will be reported earlier, it's easier to notice that some info
is missing than some extra info is leaked.
2025-12-09 03:54:54 -03:00
link2xt
5902fe2cbe refactor: remove EncryptHelper.prefer_encrypt
It always had the same value of EncryptPreference::Mutual
2025-12-07 15:33:36 +00:00
link2xt
73e0f81e83 test: port test_synchronize_member_list_on_group_rejoin to JSON-RPC 2025-12-07 14:21:48 +00:00
link2xt
cbe842735e api(rpc-client): add Chat.num_contacts() 2025-12-07 14:21:48 +00:00
link2xt
72bc9f0ae4 api(rpc-client): accept Account for Chat.{add,remove}_contact() 2025-12-07 14:21:48 +00:00
link2xt
0defa117a0 refactor: use u16 instead of usize to represent progress bar 2025-12-07 13:23:31 +00:00
link2xt
3821cfab0c fix: use u64 to count the number of bytes sent/received over the network
It is possible to send more than 4 GiB into network stream
or receive more than 4 GiB from it, in which case
this counter may overflow.
2025-12-07 13:23:31 +00:00
link2xt
09f159991e fix: use u64 to represent the number of bytes in backup files 2025-12-07 13:23:31 +00:00
link2xt
014d2ace76 fix: use u64 instead of usize to calculate storage usage
On 32-bit systems such as old Android phones usize is 32-bit as well
and cannot store sizes that exceed 4 GiB.
2025-12-06 13:01:46 +00:00
link2xt
646728372b chore: add RUSTSEC-2025-0134 exception to deny.toml 2025-12-06 12:56:16 +00:00
link2xt
7c30aef2ed chore(release): prepare for 2.33.0 2025-12-05 21:35:21 +00:00
link2xt
c38d02728e fix: recognize all transport addresses as own addresses
Fix get_secondary_addrs() which was using
`secondary_addrs` config that is not updated anymore.
Instead of using `secondary_addrs` config,
use the `transports` table which contains all the addresses.
2025-12-05 21:15:30 +00:00
iequidoo
dea1b414db feat: Case-insensitive search for non-ASCII chat and contact names (#7477)
This makes `Contact::get_all()` and `Chatlist::try_load()` case-insensitive for non-ASCII chat and
contact names as well. The same approach as in f6f4ccc6ea "feat:
Case-insensitive search for non-ASCII messages (#5052)" is used: `chats.name_normalized` and
`contacts.name_normalized` colums are added which store lowercased/normalized names (for a contact,
if the name is unset, it's a normalized authname). If a normalized name is the same as the
chat/contact name, it's not stored to reduce the db size. A db migration is added for 10000 random
chats and the same number of the most recently seen contacts, for users it will probably migrate all
chats/contacts and for bots which may have more data it's not important.
2025-12-05 05:11:29 -03:00
link2xt
aa5ee19340 chore(release): prepare for 2.32.0 2025-12-04 21:00:59 +00:00
iequidoo
9271ecd208 feat: lookup_or_create_adhoc_group(): Add context to SQL errors (#7554) 2025-12-04 17:17:04 -03:00
link2xt
952f6735a2 chore(release): prepare for 2.31.0 2025-12-04 19:28:31 +00:00
link2xt
a50aa3b6e9 ci: update npm before publishing packages
Newer npm is apparently needed for "trusted publishing".
2025-12-04 19:26:25 +00:00
link2xt
23d95df66a feat: use v2 SEIPD when sending messages to self 2025-12-04 18:25:30 +00:00
link2xt
6db2cf6144 chore(release): prepare for 2.30.0 2025-12-04 17:01:28 +00:00
link2xt
47c1e54219 ci: use "trusted publishing" for NPM packages
See the documentation at <https://docs.npmjs.com/trusted-publishers>.
I have removed the token that was used since <https://github.com/chatmail/core/pull/5575>,
created two new GitHub deployment environments and configured
trusted publishing for two packages (see the environment URLs) on https://www.npmjs.com/
2025-12-04 15:40:28 +00:00
d2weber
b41c309e21 fix: shutdown shortly after call 2025-12-04 15:37:33 +00:00
link2xt
f7ae2abe52 feat: synchronize transports via sync messages 2025-12-04 14:40:36 +00:00
link2xt
3a7f82c66e api: add TransportsModified event 2025-12-04 14:40:36 +00:00
holger krekel
d75a78d446 feat: introduce cross-core testing along with improvements to test frameworking 2025-12-04 14:29:16 +00:00
89 changed files with 2530 additions and 1142 deletions

View File

@@ -20,7 +20,7 @@ permissions: {}
env:
RUSTFLAGS: -Dwarnings
RUST_VERSION: 1.91.0
RUST_VERSION: 1.92.0
# Minimum Supported Rust Version
MSRV: 1.88.0
@@ -40,7 +40,7 @@ jobs:
- run: rustup override set $RUST_VERSION
shell: bash
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2
uses: swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
- name: Run rustfmt
run: cargo fmt --all -- --check
- name: Run clippy
@@ -59,7 +59,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: EmbarkStudios/cargo-deny-action@v2
- uses: EmbarkStudios/cargo-deny-action@76cd80eb775d7bbbd2d80292136d74d39e1b4918
with:
arguments: --all-features --workspace
command: check
@@ -91,7 +91,7 @@ jobs:
show-progress: false
persist-credentials: false
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2
uses: swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
- name: Rustdoc
run: cargo doc --document-private-items --no-deps
@@ -134,10 +134,10 @@ jobs:
shell: bash
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2
uses: swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
- name: Install nextest
uses: taiki-e/install-action@v2
uses: taiki-e/install-action@69e777b377e4ec209ddad9426ae3e0c1008b0ef3
with:
tool: nextest
@@ -168,13 +168,13 @@ jobs:
persist-credentials: false
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2
uses: swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
- name: Build C library
run: cargo build -p deltachat_ffi
- name: Upload C library
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.os }}-libdeltachat.a
path: target/debug/libdeltachat.a
@@ -194,13 +194,13 @@ jobs:
persist-credentials: false
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2
uses: swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
- name: Build deltachat-rpc-server
run: cargo build -p deltachat-rpc-server
- name: Upload deltachat-rpc-server
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.os }}-deltachat-rpc-server
path: ${{ matrix.os == 'windows-latest' && 'target/debug/deltachat-rpc-server.exe' || 'target/debug/deltachat-rpc-server' }}
@@ -243,7 +243,7 @@ jobs:
persist-credentials: false
- name: Download libdeltachat.a
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: ubuntu-latest-libdeltachat.a
path: target/debug
@@ -293,7 +293,7 @@ jobs:
persist-credentials: false
- name: Download libdeltachat.a
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: ${{ matrix.os }}-libdeltachat.a
path: target/debug
@@ -355,7 +355,7 @@ jobs:
run: pip install tox
- name: Download deltachat-rpc-server
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: ${{ matrix.os }}-deltachat-rpc-server
path: target/debug

View File

@@ -34,13 +34,13 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
- name: Upload binary
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: deltachat-rpc-server-${{ matrix.arch }}-linux
path: result/bin/deltachat-rpc-server
@@ -58,13 +58,13 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
- name: Build deltachat-rpc-server wheels
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
- name: Upload wheel
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
path: result/*.whl
@@ -82,13 +82,13 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
- name: Upload binary
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: deltachat-rpc-server-${{ matrix.arch }}
path: result/bin/deltachat-rpc-server.exe
@@ -106,13 +106,13 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
- name: Build deltachat-rpc-server wheels
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-wheel
- name: Upload wheel
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: deltachat-rpc-server-${{ matrix.arch }}-wheel
path: result/*.whl
@@ -139,7 +139,7 @@ jobs:
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.arch }}-apple-darwin --features vendored
- name: Upload binary
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: deltachat-rpc-server-${{ matrix.arch }}-macos
path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server
@@ -157,13 +157,13 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
- name: Upload binary
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: deltachat-rpc-server-${{ matrix.arch }}-android
path: result/bin/deltachat-rpc-server
@@ -181,13 +181,13 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
- name: Build deltachat-rpc-server wheels
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android-wheel
- name: Upload wheel
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: deltachat-rpc-server-${{ matrix.arch }}-android-wheel
path: result/*.whl
@@ -208,124 +208,124 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
- name: Download Linux aarch64 binary
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-aarch64-linux
path: deltachat-rpc-server-aarch64-linux.d
- name: Download Linux aarch64 wheel
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-aarch64-linux-wheel
path: deltachat-rpc-server-aarch64-linux-wheel.d
- name: Download Linux armv7l binary
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-armv7l-linux
path: deltachat-rpc-server-armv7l-linux.d
- name: Download Linux armv7l wheel
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-armv7l-linux-wheel
path: deltachat-rpc-server-armv7l-linux-wheel.d
- name: Download Linux armv6l binary
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-armv6l-linux
path: deltachat-rpc-server-armv6l-linux.d
- name: Download Linux armv6l wheel
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-armv6l-linux-wheel
path: deltachat-rpc-server-armv6l-linux-wheel.d
- name: Download Linux i686 binary
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-i686-linux
path: deltachat-rpc-server-i686-linux.d
- name: Download Linux i686 wheel
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-i686-linux-wheel
path: deltachat-rpc-server-i686-linux-wheel.d
- name: Download Linux x86_64 binary
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-x86_64-linux
path: deltachat-rpc-server-x86_64-linux.d
- name: Download Linux x86_64 wheel
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-x86_64-linux-wheel
path: deltachat-rpc-server-x86_64-linux-wheel.d
- name: Download Win32 binary
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-win32
path: deltachat-rpc-server-win32.d
- name: Download Win32 wheel
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-win32-wheel
path: deltachat-rpc-server-win32-wheel.d
- name: Download Win64 binary
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-win64
path: deltachat-rpc-server-win64.d
- name: Download Win64 wheel
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-win64-wheel
path: deltachat-rpc-server-win64-wheel.d
- name: Download macOS binary for x86_64
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-x86_64-macos
path: deltachat-rpc-server-x86_64-macos.d
- name: Download macOS binary for aarch64
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-aarch64-macos
path: deltachat-rpc-server-aarch64-macos.d
- name: Download Android binary for arm64-v8a
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-arm64-v8a-android
path: deltachat-rpc-server-arm64-v8a-android.d
- name: Download Android wheel for arm64-v8a
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-arm64-v8a-android-wheel
path: deltachat-rpc-server-arm64-v8a-android-wheel.d
- name: Download Android binary for armeabi-v7a
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-armeabi-v7a-android
path: deltachat-rpc-server-armeabi-v7a-android.d
- name: Download Android wheel for armeabi-v7a
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-armeabi-v7a-android-wheel
path: deltachat-rpc-server-armeabi-v7a-android-wheel.d
@@ -382,12 +382,15 @@ jobs:
- name: Publish deltachat-rpc-server to PyPI
if: github.event_name == 'release'
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e
publish_npm_package:
name: Build & Publish npm prebuilds and deltachat-rpc-server
needs: ["build_linux", "build_windows", "build_macos"]
runs-on: "ubuntu-latest"
environment:
name: npm-stdio-rpc-server
url: https://www.npmjs.com/package/@deltachat/stdio-rpc-server
permissions:
id-token: write
@@ -403,67 +406,67 @@ jobs:
python-version: "3.11"
- name: Download Linux aarch64 binary
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-aarch64-linux
path: deltachat-rpc-server-aarch64-linux.d
- name: Download Linux armv7l binary
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-armv7l-linux
path: deltachat-rpc-server-armv7l-linux.d
- name: Download Linux armv6l binary
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-armv6l-linux
path: deltachat-rpc-server-armv6l-linux.d
- name: Download Linux i686 binary
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-i686-linux
path: deltachat-rpc-server-i686-linux.d
- name: Download Linux x86_64 binary
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-x86_64-linux
path: deltachat-rpc-server-x86_64-linux.d
- name: Download Win32 binary
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-win32
path: deltachat-rpc-server-win32.d
- name: Download Win64 binary
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-win64
path: deltachat-rpc-server-win64.d
- name: Download macOS binary for x86_64
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-x86_64-macos
path: deltachat-rpc-server-x86_64-macos.d
- name: Download macOS binary for aarch64
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-aarch64-macos
path: deltachat-rpc-server-aarch64-macos.d
- name: Download Android binary for arm64-v8a
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-arm64-v8a-android
path: deltachat-rpc-server-arm64-v8a-android.d
- name: Download Android binary for armeabi-v7a
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: deltachat-rpc-server-armeabi-v7a-android
path: deltachat-rpc-server-armeabi-v7a-android.d
@@ -493,7 +496,7 @@ jobs:
ls -lah
- name: Upload to artifacts
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: deltachat-rpc-server-npm-package
path: deltachat-rpc-server/npm-package/*.tgz
@@ -515,11 +518,14 @@ jobs:
node-version: 20
registry-url: "https://registry.npmjs.org"
# Ensure npm 11.5.1 or later is installed.
# It is needed for <https://docs.npmjs.com/trusted-publishers>
- name: Update npm
run: npm install -g npm@latest
- name: Publish npm packets for prebuilds and `@deltachat/stdio-rpc-server`
if: github.event_name == 'release'
working-directory: deltachat-rpc-server/npm-package
run: |
ls -lah platform_package
for platform in *.tgz; do npm publish --provenance "$platform" --access public; done
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -10,6 +10,9 @@ jobs:
pack-module:
name: "Publish @deltachat/jsonrpc-client"
runs-on: ubuntu-latest
environment:
name: npm-jsonrpc-client
url: https://www.npmjs.com/package/@deltachat/jsonrpc-client
permissions:
id-token: write
contents: read
@@ -24,6 +27,11 @@ jobs:
node-version: 20
registry-url: "https://registry.npmjs.org"
# Ensure npm 11.5.1 or later is installed.
# It is needed for <https://docs.npmjs.com/trusted-publishers>
- name: Update npm
run: npm install -g npm@latest
- name: Install dependencies without running scripts
working-directory: deltachat-jsonrpc/typescript
run: npm install --ignore-scripts
@@ -37,5 +45,3 @@ jobs:
- name: Publish
working-directory: deltachat-jsonrpc/typescript
run: npm publish --provenance deltachat-jsonrpc-client-* --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -25,7 +25,7 @@ jobs:
with:
node-version: 18.x
- name: Add Rust cache
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5
- name: npm install
working-directory: deltachat-jsonrpc/typescript
run: npm install

View File

@@ -25,7 +25,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
- run: nix fmt flake.nix -- --check
build:
@@ -84,7 +84,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
- run: nix build .#${{ matrix.installable }}
build-macos:
@@ -105,5 +105,5 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
- run: nix build .#${{ matrix.installable }}

View File

@@ -23,7 +23,7 @@ jobs:
working-directory: deltachat-rpc-client
run: python3 -m build
- name: Store the distribution packages
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: python-package-distributions
path: deltachat-rpc-client/dist/
@@ -42,9 +42,9 @@ jobs:
steps:
- name: Download all the dists
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: python-package-distributions
path: dist/
- name: Publish deltachat-rpc-client to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e

View File

@@ -18,11 +18,11 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
- name: Build
run: nix build .#deltachat-repl-win64
- name: Upload binary
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: repl.exe
path: "result/bin/deltachat-repl.exe"

View File

@@ -36,7 +36,7 @@ jobs:
show-progress: false
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
- name: Build Python documentation
run: nix build .#python-docs
- name: Upload to py.delta.chat
@@ -55,7 +55,7 @@ jobs:
show-progress: false
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
- name: Build C documentation
run: nix build .#docs
- name: Upload to c.delta.chat

View File

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

View File

@@ -1,5 +1,195 @@
# Changelog
## [2.36.0] - 2026-01-03
### CI
- Pin GitHub Action references.
### API-Changes
- Add transports event to FFI.
### Features / Changes
- Add core version to `receive_imf` failure message.
- Connectivity view: quota for all transports ([#7630](https://github.com/chatmail/core/pull/7630)).
- Send sync messages over SMTP and do not move them to mvbox.
### Fixes
- When accepting group, add members with `Origin::IncomingTo` and sort them down in the contact list (7592).
- Update fallback welcome message.
- `inner_configure`: Check Config::OnlyFetchMvbox before MvboxMove for multi-transport ([#7637](https://github.com/chatmail/core/pull/7637)).
- Reset options not available for chatmail on chatmail profiles.
- Don't send webxdc notification for `notify: "*"` when chat is muted ([#7658](https://github.com/chatmail/core/pull/7658)).
### Documentation
- `delete_chat()`: don't lie that messages aren't deleted from server.
- Remove references to removed `sentbox_watch` config.
- Update documentation for `TransportsModified` event.
### Tests
- Contact list after accepting group with unknown contacts ([#7592](https://github.com/chatmail/core/pull/7592)).
- Port test_import_export_online_all to JSON-RPC ([#7411](https://github.com/chatmail/core/pull/7411)).
### Refactor
- Turn `DC_VERSION_STR` into `&str`.
- ffi: Remove one pointer indirection for `dc_accounts_t`.
### Miscellaneous Tasks
- deps: Bump actions/download-artifact from 6 to 7.
- deps: Bump actions/upload-artifact from 5 to 6.
- deps: Bump astral-sh/setup-uv from 7.1.4 to 7.1.6.
- deps: Bump cachix/install-nix-action from 31.8.4 to 31.9.0.
- cargo: Bump serde_json from 1.0.145 to 1.0.147.
- cargo: Bump uuid from 1.18.1 to 1.19.0.
- cargo: Bump toml from 0.9.8 to 0.9.10+spec-1.1.0.
- cargo: Bump tempfile from 3.23.0 to 3.24.0.
- cargo: Bump libc from 0.2.177 to 0.2.178.
- cargo: Bump tracing from 0.1.41 to 0.1.44.
- cargo: Bump hyper-util from 0.1.18 to 0.1.19.
- cargo: Bump log from 0.4.28 to 0.4.29.
- cargo: Bump rustls-pki-types from 1.13.0 to 1.13.2.
- cargo: Bump criterion from 0.7.0 to 0.8.1.
## [2.35.0] - 2025-12-16
### API-Changes
- Add blob dir size to storage info ([#7605](https://github.com/chatmail/core/pull/7605)).
### Features / Changes
- Use `turn.delta.chat` as fallback TURN server ([#7382](https://github.com/chatmail/core/pull/7382)).
- Add ip addresses of known public chatmail relays from https://chatmail.at/relays to DNS cache ([#7607](https://github.com/chatmail/core/pull/7607)).
- Improve error messages on adding relays.
- Add transport addresses to IMAP URLs in message info.
- `lookup_host_with_cache()`: Don't return empty address list ([#7596](https://github.com/chatmail/core/pull/7596)).
### Fixes
- `get_chat_msgs_ex()`: Don't match on "S=" (Cmd) in param payload.
- Remove `SecurejoinWait` info message when received Alice's key ([#7585](https://github.com/chatmail/core/pull/7585)).
- Do not set normalized name for existing chats and contacts in a migration.
- Remove now redundant "used_account_settings" and "entered_account_settings" from `Context.get_info()` ([#7587](https://github.com/chatmail/core/pull/7587)).
- Don't use fallback servers if got TURN servers from IMAP METADATA.
- Use fallback ICE servers if server can't IMAP METADATA ([#7382](https://github.com/chatmail/core/pull/7382)).
- Add explicit limit for adding relays (5 at the moment) ([#7611](https://github.com/chatmail/core/pull/7611)).
- Take `transport_id` into account when using `imap` table.
### CI
- Update Rust to 1.92.0.
### Miscellaneous Tasks
- Apply Rust 1.92.0 clippy suggestions.
### Other
- Log entered login params and actual used params on configuration failure ([#7610](https://github.com/chatmail/core/pull/7610)).
## [2.34.0] - 2025-12-11
### API-Changes
- rpc-client: Accept `Account` for `Chat.{add,remove}_contact()`.
- rpc-client: Add `Chat.num_contacts()`.
- Forwarding messages to another profile ([#7491](https://github.com/chatmail/core/pull/7491)).
### Features / Changes
- Double ringing time to 120 seconds.
- Better logging for failing securejoin messages ([#7593](https://github.com/chatmail/core/pull/7593)).
- Add multi-transport information to `Context.get_info` ([#7583](https://github.com/chatmail/core/pull/7583))
### Fixes
- Multi-transport: all transports were shown as "inbox" in connectivity view, now they are shown by their hostname ([#7582](https://github.com/chatmail/core/pull/7582)).
- Multi-transport: Synchronize primary transport immediately after changing it.
- Use u64 instead of usize to calculate storage usage.
- Use u64 to represent the number of bytes in backup files.
- Use u64 to count the number of bytes sent/received over the network.
- Use logging macros instead of emitting event directly, so that it is also logged by tracing ([#7459](https://github.com/chatmail/core/pull/7459)).
- Let securejoin succeed even if the chat was deleted in the meantime ([#7594](https://github.com/chatmail/core/pull/7594)).
### Miscellaneous Tasks
- Add RUSTSEC-2025-0134 exception to deny.toml.
### Refactor
- Use u16 instead of usize to represent progress bar.
- Remove EncryptHelper.prefer_encrypt.
- Add params when forwarding message instead of removing unneeded ones.
### Tests
- Port test_synchronize_member_list_on_group_rejoin to JSON-RPC.
- Test setting up second device between core versions.
## [2.33.0] - 2025-12-05
### Features / Changes
- Case-insensitive search for non-ASCII chat and contact names ([#7477](https://github.com/chatmail/core/pull/7477)).
### Fixes
- Recognize all transport addresses as own addresses.
## [2.32.0] - 2025-12-04
Version bump to trigger publishing of npm prebuilds
that failed to be published for 2.31.0 due to not configured "trusted publishers".
### Features / Changes
- Lookup_or_create_adhoc_group(): Add context to SQL errors ([#7554](https://github.com/chatmail/core/pull/7554)).
## [2.31.0] - 2025-12-04
### CI
- Update npm before publishing packages.
### Features / Changes
- Use v2 SEIPD when sending messages to self.
## [2.30.0] - 2025-12-04
### Features / Changes
- Disable SNI for STARTTLS ([#7499](https://github.com/chatmail/core/pull/7499)).
- Introduce cross-core testing along with improvements to test frameworking.
- Synchronize transports via sync messages.
### Fixes
- Fix shutdown shortly after call.
### API-Changes
- Add `TransportsModified` event (for tests).
### CI
- Use "trusted publishing" for NPM packages.
### Miscellaneous Tasks
- deps: Bump actions/checkout from 5 to 6.
- cargo: Bump syn from 2.0.110 to 2.0.111.
- deps: Bump astral-sh/setup-uv from 7.1.3 to 7.1.4.
- cargo: Bump sdp from 0.8.0 to 0.10.0.
- Remove two outdated todo comments ([#7550](https://github.com/chatmail/core/pull/7550)).
## [2.29.0] - 2025-12-01
### API-Changes
@@ -7310,3 +7500,10 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[2.27.0]: https://github.com/chatmail/core/compare/v2.26.0..v2.27.0
[2.28.0]: https://github.com/chatmail/core/compare/v2.27.0..v2.28.0
[2.29.0]: https://github.com/chatmail/core/compare/v2.28.0..v2.29.0
[2.30.0]: https://github.com/chatmail/core/compare/v2.29.0..v2.30.0
[2.31.0]: https://github.com/chatmail/core/compare/v2.30.0..v2.31.0
[2.32.0]: https://github.com/chatmail/core/compare/v2.31.0..v2.32.0
[2.33.0]: https://github.com/chatmail/core/compare/v2.32.0..v2.33.0
[2.34.0]: https://github.com/chatmail/core/compare/v2.33.0..v2.34.0
[2.35.0]: https://github.com/chatmail/core/compare/v2.34.0..v2.35.0
[2.36.0]: https://github.com/chatmail/core/compare/v2.35.0..v2.36.0

129
Cargo.lock generated
View File

@@ -98,6 +98,15 @@ dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "alloca"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4"
dependencies = [
"cc",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
@@ -1042,10 +1051,11 @@ dependencies = [
[[package]]
name = "criterion"
version = "0.7.0"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1c047a62b0cc3e145fa84415a3191f628e980b194c2755aa12300a4e6cbd928"
checksum = "4d883447757bb0ee46f233e9dc22eb84d93a9508c9b868687b274fc431d886bf"
dependencies = [
"alloca",
"anes",
"cast",
"ciborium",
@@ -1054,6 +1064,7 @@ dependencies = [
"itertools",
"num-traits",
"oorandom",
"page_size",
"plotters",
"rayon",
"regex",
@@ -1066,9 +1077,9 @@ dependencies = [
[[package]]
name = "criterion-plot"
version = "0.6.0"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338"
checksum = "ed943f81ea2faa8dcecbbfa50164acf95d555afec96a27871663b300e387b2e4"
dependencies = [
"cast",
"itertools",
@@ -1304,7 +1315,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "2.29.0"
version = "2.36.0"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -1388,6 +1399,7 @@ dependencies = [
"tracing",
"url",
"uuid",
"walkdir",
"webpki-roots",
]
@@ -1413,7 +1425,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "2.29.0"
version = "2.36.0"
dependencies = [
"anyhow",
"async-channel 2.5.0",
@@ -1429,13 +1441,12 @@ dependencies = [
"tempfile",
"tokio",
"typescript-type-def",
"walkdir",
"yerpc",
]
[[package]]
name = "deltachat-repl"
version = "2.29.0"
version = "2.36.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1451,7 +1462,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "2.29.0"
version = "2.36.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1480,7 +1491,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "2.29.0"
version = "2.36.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1997,7 +2008,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78"
dependencies = [
"cfg-if",
"rustix 1.0.5",
"rustix 1.1.3",
"windows-sys 0.59.0",
]
@@ -2648,9 +2659,9 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.18"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56"
checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
dependencies = [
"bytes",
"futures-channel",
@@ -3267,9 +3278,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.177"
version = "0.2.178"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
[[package]]
name = "libm"
@@ -3317,9 +3328,9 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "linux-raw-sys"
version = "0.9.3"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
[[package]]
name = "litemap"
@@ -3344,9 +3355,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.28"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "loom"
@@ -4023,6 +4034,16 @@ dependencies = [
"sha2",
]
[[package]]
name = "page_size"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "parking"
version = "2.2.1"
@@ -5127,15 +5148,15 @@ dependencies = [
[[package]]
name = "rustix"
version = "1.0.5"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf"
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
dependencies = [
"bitflags 2.9.1",
"errno",
"libc",
"linux-raw-sys 0.9.3",
"windows-sys 0.59.0",
"linux-raw-sys 0.11.0",
"windows-sys 0.61.1",
]
[[package]]
@@ -5164,9 +5185,9 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
version = "1.13.0"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a"
checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282"
dependencies = [
"web-time",
"zeroize",
@@ -5422,22 +5443,22 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.145"
version = "1.0.148"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "serde_spanned"
version = "1.0.3"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392"
checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
dependencies = [
"serde_core",
]
@@ -5947,14 +5968,14 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tempfile"
version = "3.23.0"
version = "3.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
dependencies = [
"fastrand",
"getrandom 0.3.3",
"once_cell",
"rustix 1.0.5",
"rustix 1.1.3",
"windows-sys 0.61.1",
]
@@ -6217,14 +6238,14 @@ dependencies = [
[[package]]
name = "toml"
version = "0.9.8"
version = "0.9.10+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8"
checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48"
dependencies = [
"indexmap",
"serde_core",
"serde_spanned",
"toml_datetime 0.7.3",
"toml_datetime 0.7.5+spec-1.1.0",
"toml_parser",
"toml_writer",
"winnow 0.7.13",
@@ -6238,9 +6259,9 @@ checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
[[package]]
name = "toml_datetime"
version = "0.7.3"
version = "0.7.5+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533"
checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
dependencies = [
"serde_core",
]
@@ -6270,9 +6291,9 @@ dependencies = [
[[package]]
name = "toml_parser"
version = "1.0.4"
version = "1.0.6+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e"
checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
dependencies = [
"winnow 0.7.13",
]
@@ -6285,9 +6306,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "toml_writer"
version = "1.0.4"
version = "1.0.6+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
[[package]]
name = "tower"
@@ -6318,9 +6339,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
version = "0.1.41"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"log",
"pin-project-lite",
@@ -6330,9 +6351,9 @@ dependencies = [
[[package]]
name = "tracing-attributes"
version = "0.1.28"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
@@ -6341,9 +6362,9 @@ dependencies = [
[[package]]
name = "tracing-core"
version = "0.1.33"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
"valuable",
@@ -6542,13 +6563,13 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.18.1"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a"
dependencies = [
"getrandom 0.3.3",
"js-sys",
"serde",
"serde_core",
"wasm-bindgen",
]
@@ -7465,6 +7486,12 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "868b928d7949e09af2f6086dfc1e01936064cc7a819253bce650d4e2a2d63ba8"
[[package]]
name = "zmij"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aac060176f7020d62c3bcc1cdbcec619d54f48b07ad1963a3f80ce7a0c17755f"
[[package]]
name = "zune-core"
version = "0.5.0"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "2.29.0"
version = "2.36.0"
edition = "2024"
license = "MPL-2.0"
rust-version = "1.88"
@@ -111,11 +111,12 @@ toml = "0.9"
tracing = "0.1.41"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
walkdir = "2.5.0"
webpki-roots = "0.26.8"
[dev-dependencies]
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
criterion = { version = "0.7.0", features = ["async_tokio"] }
criterion = { version = "0.8.1", features = ["async_tokio"] }
futures-lite = { workspace = true }
log = { workspace = true }
nu-ansi-term = { workspace = true }
@@ -198,7 +199,7 @@ rusqlite = "0.37"
sanitize-filename = "0.6"
serde = "1.0"
serde_json = "1"
tempfile = "3.23.0"
tempfile = "3.24.0"
thiserror = "2"
tokio = "1"
tokio-util = "0.7.17"

View File

@@ -16,7 +16,8 @@ id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT DEFAULT '' NOT NULL -- message text
) STRICT",
)
.await?;
.await
.context("CREATE TABLE messages")?;
```
Do not use macros like [`concat!`](https://doc.rust-lang.org/std/macro.concat.html)
@@ -29,7 +30,8 @@ id INTEGER PRIMARY KEY AUTOINCREMENT, \
text TEXT DEFAULT '' NOT NULL \
) STRICT",
)
.await?;
.await
.context("CREATE TABLE messages")?;
```
Escaping newlines
is prone to errors like this if space before backslash is missing:
@@ -63,6 +65,9 @@ an older version. Also don't change the column type, consider adding a new colum
instead. Finally, never change column semantics, this is especially dangerous because the `STRICT`
keyword doesn't help here.
Consider adding context to `anyhow` errors for SQL statements using `.context()` so that it's
possible to understand from logs which statement failed. See [Errors](#errors) for more info.
## Errors
Delta Chat core mostly uses [`anyhow`](https://docs.rs/anyhow/) errors.

View File

@@ -38,7 +38,7 @@ use deltachat::{
internals_for_benches::key_from_asc,
internals_for_benches::parse_and_get_text,
internals_for_benches::store_self_keypair,
pgp::{KeyPair, decrypt, pk_encrypt, symm_encrypt_message},
pgp::{KeyPair, SeipdVersion, decrypt, pk_encrypt, symm_encrypt_message},
stock_str::StockStrings,
};
use rand::{Rng, rng};
@@ -111,6 +111,7 @@ fn criterion_benchmark(c: &mut Criterion) {
key_pair.secret.clone(),
true,
true,
SeipdVersion::V2,
)
.await
.unwrap()

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "2.29.0"
version = "2.36.0"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"

View File

@@ -429,16 +429,13 @@ char* dc_get_blobdir (const dc_context_t* context);
* 1=send a copy of outgoing messages to self (default).
* Sending messages to self is needed for a proper multi-account setup,
* however, on the other hand, may lead to unwanted notifications in non-delta clients.
* - `sentbox_watch`= 1=watch `Sent`-folder for changes,
* 0=do not watch the `Sent`-folder (default).
* - `mvbox_move` = 1=detect chat messages,
* move them to the `DeltaChat` folder,
* and watch the `DeltaChat` folder for updates (default),
* 0=do not move chat-messages
* - `only_fetch_mvbox` = 1=Do not fetch messages from folders other than the
* `DeltaChat` folder. Messages will still be fetched from the
* spam folder and `sendbox_watch` will also still be respected
* if enabled.
* spam folder.
* 0=watch all folders normally (default)
* - `show_emails` = DC_SHOW_EMAILS_OFF (0)=
* show direct replies to chats only,
@@ -1611,10 +1608,10 @@ void dc_set_chat_visibility (dc_context_t* context, uint32_t ch
*
* Messages are deleted from the device and the chat database entry is deleted.
* After that, the event #DC_EVENT_MSGS_CHANGED is posted.
* Messages are deleted from the server in background.
*
* Things that are _not_ done implicitly:
*
* - Messages are **not deleted from the server**.
* - The chat or the contact is **not blocked**, so new messages from the user/the group may appear
* and the user may create the chat again.
* - **Groups are not left** - this would
@@ -6702,6 +6699,16 @@ void dc_event_unref(dc_event_t* event);
*/
#define DC_EVENT_CALL_ENDED 2580
/**
* Transport relay added/deleted or default has changed.
* UI should update the list.
*
* The event is emitted when the transports are modified on another device
* using the JSON-RPC calls `add_or_update_transport`, `add_transport_from_qr`, `delete_transport`
* or `set_config(configured_addr)`.
*/
#define DC_EVENT_TRANSPORTS_MODIFIED 2600
/**
* @}
@@ -7304,12 +7311,6 @@ void dc_event_unref(dc_event_t* event);
/// Used as a headline in the connectivity view.
#define DC_STR_OUTGOING_MESSAGES 104
/// "Storage on %1$s"
///
/// Used as a headline in the connectivity view.
///
/// `%1$s` will be replaced by the domain of the configured e-mail address.
#define DC_STR_STORAGE_ON_DOMAIN 105
/// @deprecated Deprecated 2022-04-16, this string is no longer needed.
#define DC_STR_ONE_MOMENT 106

View File

@@ -15,7 +15,6 @@ use std::collections::BTreeMap;
use std::convert::TryFrom;
use std::fmt::Write;
use std::future::Future;
use std::ops::Deref;
use std::ptr;
use std::str::FromStr;
use std::sync::{Arc, LazyLock};
@@ -559,6 +558,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::IncomingCallAccepted { .. } => 2560,
EventType::OutgoingCallAccepted { .. } => 2570,
EventType::CallEnded { .. } => 2580,
EventType::TransportsModified => 2600,
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
@@ -593,7 +593,8 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::AccountsBackgroundFetchDone
| EventType::ChatlistChanged
| EventType::AccountsChanged
| EventType::AccountsItemChanged => 0,
| EventType::AccountsItemChanged
| EventType::TransportsModified => 0,
EventType::IncomingReaction { contact_id, .. }
| EventType::IncomingWebxdcNotify { contact_id, .. } => contact_id.to_u32() as libc::c_int,
EventType::MsgsChanged { chat_id, .. }
@@ -681,7 +682,8 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::IncomingCallAccepted { .. }
| EventType::OutgoingCallAccepted { .. }
| EventType::CallEnded { .. }
| EventType::EventChannelOverflow { .. } => 0,
| EventType::EventChannelOverflow { .. }
| EventType::TransportsModified => 0,
EventType::MsgsChanged { msg_id, .. }
| EventType::ReactionsChanged { msg_id, .. }
| EventType::IncomingReaction { msg_id, .. }
@@ -780,7 +782,8 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
| EventType::AccountsChanged
| EventType::AccountsItemChanged
| EventType::IncomingCallAccepted { .. }
| EventType::WebxdcRealtimeAdvertisementReceived { .. } => ptr::null_mut(),
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
| EventType::TransportsModified => ptr::null_mut(),
EventType::IncomingCall {
place_call_info, ..
} => {
@@ -4735,33 +4738,13 @@ pub unsafe extern "C" fn dc_provider_unref(provider: *mut dc_provider_t) {
/// Reader-writer lock wrapper for accounts manager to guarantee thread safety when using
/// `dc_accounts_t` in multiple threads at once.
pub struct AccountsWrapper {
inner: Arc<RwLock<Accounts>>,
}
impl Deref for AccountsWrapper {
type Target = Arc<RwLock<Accounts>>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl AccountsWrapper {
fn new(accounts: Accounts) -> Self {
let inner = Arc::new(RwLock::new(accounts));
Self { inner }
}
}
/// Struct representing a list of deltachat accounts.
pub type dc_accounts_t = AccountsWrapper;
pub type dc_accounts_t = RwLock<Accounts>;
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_new(
dir: *const libc::c_char,
writable: libc::c_int,
) -> *mut dc_accounts_t {
) -> *const dc_accounts_t {
setup_panic!();
if dir.is_null() {
@@ -4772,7 +4755,7 @@ pub unsafe extern "C" fn dc_accounts_new(
let accs = block_on(Accounts::new(as_path(dir).into(), writable != 0));
match accs {
Ok(accs) => Box::into_raw(Box::new(AccountsWrapper::new(accs))),
Ok(accs) => Arc::into_raw(Arc::new(RwLock::new(accs))),
Err(err) => {
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
eprintln!("failed to create accounts: {err:#}");
@@ -4785,17 +4768,17 @@ pub unsafe extern "C" fn dc_accounts_new(
///
/// This function releases the memory of the `dc_accounts_t` structure.
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_unref(accounts: *mut dc_accounts_t) {
pub unsafe extern "C" fn dc_accounts_unref(accounts: *const dc_accounts_t) {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_unref()");
return;
}
let _ = Box::from_raw(accounts);
let _ = Arc::from_raw(accounts);
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_get_account(
accounts: *mut dc_accounts_t,
accounts: *const dc_accounts_t,
id: u32,
) -> *mut dc_context_t {
if accounts.is_null() {
@@ -4812,7 +4795,7 @@ pub unsafe extern "C" fn dc_accounts_get_account(
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_get_selected_account(
accounts: *mut dc_accounts_t,
accounts: *const dc_accounts_t,
) -> *mut dc_context_t {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_get_selected_account()");
@@ -4828,7 +4811,7 @@ pub unsafe extern "C" fn dc_accounts_get_selected_account(
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_select_account(
accounts: *mut dc_accounts_t,
accounts: *const dc_accounts_t,
id: u32,
) -> libc::c_int {
if accounts.is_null() {
@@ -4852,13 +4835,13 @@ pub unsafe extern "C" fn dc_accounts_select_account(
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_add_account(accounts: *mut dc_accounts_t) -> u32 {
pub unsafe extern "C" fn dc_accounts_add_account(accounts: *const dc_accounts_t) -> u32 {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_add_account()");
return 0;
}
let accounts = &mut *accounts;
let accounts = &*accounts;
block_on(async move {
let mut accounts = accounts.write().await;
@@ -4873,13 +4856,13 @@ pub unsafe extern "C" fn dc_accounts_add_account(accounts: *mut dc_accounts_t) -
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_add_closed_account(accounts: *mut dc_accounts_t) -> u32 {
pub unsafe extern "C" fn dc_accounts_add_closed_account(accounts: *const dc_accounts_t) -> u32 {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_add_closed_account()");
return 0;
}
let accounts = &mut *accounts;
let accounts = &*accounts;
block_on(async move {
let mut accounts = accounts.write().await;
@@ -4895,7 +4878,7 @@ pub unsafe extern "C" fn dc_accounts_add_closed_account(accounts: *mut dc_accoun
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_remove_account(
accounts: *mut dc_accounts_t,
accounts: *const dc_accounts_t,
id: u32,
) -> libc::c_int {
if accounts.is_null() {
@@ -4903,7 +4886,7 @@ pub unsafe extern "C" fn dc_accounts_remove_account(
return 0;
}
let accounts = &mut *accounts;
let accounts = &*accounts;
block_on(async move {
let mut accounts = accounts.write().await;
@@ -4921,7 +4904,7 @@ pub unsafe extern "C" fn dc_accounts_remove_account(
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_migrate_account(
accounts: *mut dc_accounts_t,
accounts: *const dc_accounts_t,
dbfile: *const libc::c_char,
) -> u32 {
if accounts.is_null() || dbfile.is_null() {
@@ -4929,7 +4912,7 @@ pub unsafe extern "C" fn dc_accounts_migrate_account(
return 0;
}
let accounts = &mut *accounts;
let accounts = &*accounts;
let dbfile = to_string_lossy(dbfile);
block_on(async move {
@@ -4950,7 +4933,7 @@ pub unsafe extern "C" fn dc_accounts_migrate_account(
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_get_all(accounts: *mut dc_accounts_t) -> *mut dc_array_t {
pub unsafe extern "C" fn dc_accounts_get_all(accounts: *const dc_accounts_t) -> *mut dc_array_t {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_get_all()");
return ptr::null_mut();
@@ -4964,18 +4947,18 @@ pub unsafe extern "C" fn dc_accounts_get_all(accounts: *mut dc_accounts_t) -> *m
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_start_io(accounts: *mut dc_accounts_t) {
pub unsafe extern "C" fn dc_accounts_start_io(accounts: *const dc_accounts_t) {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_start_io()");
return;
}
let accounts = &mut *accounts;
let accounts = &*accounts;
block_on(async move { accounts.write().await.start_io().await });
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_stop_io(accounts: *mut dc_accounts_t) {
pub unsafe extern "C" fn dc_accounts_stop_io(accounts: *const dc_accounts_t) {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_stop_io()");
return;
@@ -4986,7 +4969,7 @@ pub unsafe extern "C" fn dc_accounts_stop_io(accounts: *mut dc_accounts_t) {
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_maybe_network(accounts: *mut dc_accounts_t) {
pub unsafe extern "C" fn dc_accounts_maybe_network(accounts: *const dc_accounts_t) {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_maybe_network()");
return;
@@ -4997,7 +4980,7 @@ pub unsafe extern "C" fn dc_accounts_maybe_network(accounts: *mut dc_accounts_t)
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *mut dc_accounts_t) {
pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *const dc_accounts_t) {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_maybe_network_lost()");
return;
@@ -5009,7 +4992,7 @@ pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *mut dc_accoun
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_background_fetch(
accounts: *mut dc_accounts_t,
accounts: *const dc_accounts_t,
timeout_in_seconds: u64,
) -> libc::c_int {
if accounts.is_null() || timeout_in_seconds <= 2 {
@@ -5028,7 +5011,7 @@ pub unsafe extern "C" fn dc_accounts_background_fetch(
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_stop_background_fetch(accounts: *mut dc_accounts_t) {
pub unsafe extern "C" fn dc_accounts_stop_background_fetch(accounts: *const dc_accounts_t) {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_stop_background_fetch()");
return;
@@ -5040,7 +5023,7 @@ pub unsafe extern "C" fn dc_accounts_stop_background_fetch(accounts: *mut dc_acc
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_set_push_device_token(
accounts: *mut dc_accounts_t,
accounts: *const dc_accounts_t,
token: *const libc::c_char,
) {
if accounts.is_null() {
@@ -5063,7 +5046,7 @@ pub unsafe extern "C" fn dc_accounts_set_push_device_token(
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_get_event_emitter(
accounts: *mut dc_accounts_t,
accounts: *const dc_accounts_t,
) -> *mut dc_event_emitter_t {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_get_event_emitter()");
@@ -5083,16 +5066,16 @@ pub struct dc_jsonrpc_instance_t {
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_init(
account_manager: *mut dc_accounts_t,
account_manager: *const dc_accounts_t,
) -> *mut dc_jsonrpc_instance_t {
if account_manager.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_init()");
return ptr::null_mut();
}
let account_manager = &*account_manager;
let account_manager = Arc::from_raw(account_manager);
let cmd_api = block_on(deltachat_jsonrpc::api::CommandApi::from_arc(
account_manager.inner.clone(),
account_manager.clone(),
));
let (request_handle, receiver) = RpcClient::new();

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "2.29.0"
version = "2.36.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
license = "MPL-2.0"
@@ -19,7 +19,6 @@ yerpc = { workspace = true, features = ["anyhow_expose", "openrpc"] }
typescript-type-def = { version = "0.5.13", features = ["json_value"] }
tokio = { workspace = true }
sanitize-filename = { workspace = true }
walkdir = "2.5.0"
base64 = { workspace = true }
[dev-dependencies]

View File

@@ -10,8 +10,9 @@ pub use deltachat::accounts::Accounts;
use deltachat::blob::BlobObject;
use deltachat::calls::ice_servers;
use deltachat::chat::{
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
self, add_contact_to_chat, forward_msgs, forward_msgs_2ctx, get_chat_media, get_chat_msgs,
get_chat_msgs_ex, marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem,
MessageListOptions,
};
use deltachat::chatlist::Chatlist;
use deltachat::config::Config;
@@ -34,14 +35,13 @@ use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
use deltachat::reaction::{get_msg_reactions, send_reaction};
use deltachat::securejoin;
use deltachat::stock_str::StockMessage;
use deltachat::storage_usage::get_storage_usage;
use deltachat::storage_usage::{get_blobdir_storage_usage, get_storage_usage};
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::EventEmitter;
use sanitize_filename::is_sanitized;
use tokio::fs;
use tokio::sync::{watch, Mutex, RwLock};
use types::login_param::EnteredLoginParam;
use walkdir::WalkDir;
use yerpc::rpc;
pub mod types;
@@ -329,13 +329,7 @@ impl CommandApi {
async fn get_account_file_size(&self, account_id: u32) -> Result<u64> {
let ctx = self.get_context(account_id).await?;
let dbfile = ctx.get_dbfile().metadata()?.len();
let total_size = WalkDir::new(ctx.get_blobdir())
.max_depth(2)
.into_iter()
.filter_map(|entry| entry.ok())
.filter_map(|entry| entry.metadata().ok())
.filter(|metadata| metadata.is_file())
.fold(0, |acc, m| acc + m.len());
let total_size = get_blobdir_storage_usage(&ctx);
Ok(dbfile + total_size)
}
@@ -801,11 +795,11 @@ impl CommandApi {
/// Delete a chat.
///
/// Messages are deleted from the device and the chat database entry is deleted.
/// After that, the event #DC_EVENT_MSGS_CHANGED is posted.
/// After that, a `MsgsChanged` event is emitted.
/// Messages are deleted from the server in background.
///
/// Things that are _not done_ implicitly:
///
/// - Messages are **not deleted from the server**.
/// - The chat or the contact is **not blocked**, so new messages from the user/the group may appear as a contact request
/// and the user may create the chat again.
/// - **Groups are not left** - this would
@@ -2208,6 +2202,27 @@ impl CommandApi {
forward_msgs(&ctx, &message_ids, ChatId::new(chat_id)).await
}
/// Forward messages to a chat in another account.
/// See [`Self::forward_messages`] for more info.
async fn forward_messages_to_account(
&self,
src_account_id: u32,
src_message_ids: Vec<u32>,
dst_account_id: u32,
dst_chat_id: u32,
) -> Result<()> {
let src_ctx = self.get_context(src_account_id).await?;
let dst_ctx = self.get_context(dst_account_id).await?;
let src_message_ids: Vec<MsgId> = src_message_ids.into_iter().map(MsgId::new).collect();
forward_msgs_2ctx(
&src_ctx,
&src_message_ids,
&dst_ctx,
ChatId::new(dst_chat_id),
)
.await
}
/// Resend messages and make information available for newly added chat members.
/// Resending sends out the original message, however, recipients and webxdc-status may differ.
/// Clients that already have the original message can still ignore the resent message as

View File

@@ -271,7 +271,7 @@ pub enum EventType {
/// Progress.
///
/// 0=error, 1-999=progress in permille, 1000=success and done
progress: usize,
progress: u16,
/// Progress comment or error, something to display to the user.
comment: Option<String>,
@@ -282,7 +282,7 @@ pub enum EventType {
#[serde(rename_all = "camelCase")]
ImexProgress {
/// 0=error, 1-999=progress in permille, 1000=success and done
progress: usize,
progress: u16,
},
/// A file has been exported. A file has been written by imex().
@@ -313,7 +313,7 @@ pub enum EventType {
chat_id: u32,
/// Progress, always 1000.
progress: usize,
progress: u16,
},
/// Progress information of a secure-join handshake from the view of the joiner
@@ -329,7 +329,7 @@ pub enum EventType {
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
/// (Bob has verified alice and waits until Alice does the same for him)
/// 1000=vg-member-added/vc-contact-confirm received
progress: usize,
progress: u16,
},
/// The connectivity to the server changed.
@@ -460,6 +460,15 @@ pub enum EventType {
/// ID of the chat which the message belongs to.
chat_id: u32,
},
/// One or more transports has changed.
///
/// UI should update the list.
///
/// This event is emitted when transport
/// synchronization messages arrives,
/// but not when the UI modifies the transport list by itself.
TransportsModified,
}
impl From<CoreEventType> for EventType {
@@ -642,6 +651,8 @@ impl From<CoreEventType> for EventType {
msg_id: msg_id.to_u32(),
chat_id: chat_id.to_u32(),
},
CoreEventType::TransportsModified => TransportsModified,
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),

View File

@@ -54,5 +54,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "2.29.0"
"version": "2.36.0"
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "2.29.0"
version = "2.36.0"
license = "MPL-2.0"
edition = "2021"
repository = "https://github.com/chatmail/core"

View File

@@ -430,12 +430,12 @@ async fn handle_cmd(
}
"oauth2" => {
if let Some(addr) = ctx.get_config(config::Config::Addr).await? {
let oauth2_url =
get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await?;
if oauth2_url.is_none() {
println!("OAuth2 not available for {}.", &addr);
if let Some(oauth2_url) =
get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await?
{
println!("Open the following url, set mail_pw to the generated token and server_flags to 2:\n{oauth2_url}");
} else {
println!("Open the following url, set mail_pw to the generated token and server_flags to 2:\n{}", oauth2_url.unwrap());
println!("OAuth2 not available for {}.", &addr);
}
} else {
println!("oauth2: set addr first.");

View File

@@ -30,6 +30,15 @@ $ pip install .
Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.
## Activating current checkout of deltachat-rpc-client and -server for development
Go to root repository directory and run:
```
$ scripts/make-rpc-testenv.sh
$ source venv/bin/activate
```
## Using in REPL
Setup a development environment:

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "2.29.0"
version = "2.36.0"
license = "MPL-2.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [

View File

@@ -219,10 +219,12 @@ class Chat:
"""Mark all messages in this chat as noticed."""
self._rpc.marknoticed_chat(self.account.id, self.id)
def add_contact(self, *contact: Union[int, str, Contact]) -> None:
def add_contact(self, *contact: Union[int, str, Contact, "Account"]) -> None:
"""Add contacts to this group."""
from .account import Account
for cnt in contact:
if isinstance(cnt, str):
if isinstance(cnt, (str, Account)):
contact_id = self.account.create_contact(cnt).id
elif not isinstance(cnt, int):
contact_id = cnt.id
@@ -230,10 +232,12 @@ class Chat:
contact_id = cnt
self._rpc.add_contact_to_chat(self.account.id, self.id, contact_id)
def remove_contact(self, *contact: Union[int, str, Contact]) -> None:
def remove_contact(self, *contact: Union[int, str, Contact, "Account"]) -> None:
"""Remove members from this group."""
from .account import Account
for cnt in contact:
if isinstance(cnt, str):
if isinstance(cnt, (str, Account)):
contact_id = self.account.create_contact(cnt).id
elif not isinstance(cnt, int):
contact_id = cnt.id
@@ -249,6 +253,10 @@ class Chat:
contacts = self._rpc.get_chat_contacts(self.account.id, self.id)
return [Contact(self.account, contact_id) for contact_id in contacts]
def num_contacts(self) -> int:
"""Return number of contacts in this chat."""
return len(self.get_contacts())
def get_past_contacts(self) -> list[Contact]:
"""Get past contacts for this chat."""
past_contacts = self._rpc.get_past_chat_contacts(self.account.id, self.id)

View File

@@ -80,6 +80,7 @@ class EventType(str, Enum):
CONFIG_SYNCED = "ConfigSynced"
WEBXDC_REALTIME_DATA = "WebxdcRealtimeData"
WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED = "WebxdcRealtimeAdvertisementReceived"
TRANSPORTS_MODIFIED = "TransportsModified"
class ChatId(IntEnum):

View File

@@ -3,9 +3,14 @@
from __future__ import annotations
import os
import pathlib
import platform
import random
import subprocess
import sys
from typing import AsyncGenerator, Optional
import execnet
import py
import pytest
@@ -20,6 +25,18 @@ Currently this is "End-to-end encryption available".
"""
def pytest_report_header():
for base in os.get_exec_path():
fn = pathlib.Path(base).joinpath(base, "deltachat-rpc-server")
if fn.exists():
proc = subprocess.Popen([str(fn), "--version"], stderr=subprocess.PIPE)
proc.wait()
version = proc.stderr.read().decode().strip()
return f"deltachat-rpc-server: {fn} [{version}]"
return None
class ACFactory:
"""Test account factory."""
@@ -197,3 +214,134 @@ def log():
print(" " + msg)
return Printer()
#
# support for testing against different deltachat-rpc-server/clients
# installed into a temporary virtualenv and connected via 'execnet' channels
#
def find_path(venv, name):
is_windows = platform.system() == "Windows"
bin = venv / ("bin" if not is_windows else "Scripts")
tryadd = [""]
if is_windows:
tryadd += os.environ["PATHEXT"].split(os.pathsep)
for ext in tryadd:
p = bin.joinpath(name + ext)
if p.exists():
return str(p)
return None
@pytest.fixture(scope="session")
def get_core_python_env(tmp_path_factory):
"""Return a factory to create virtualenv environments with rpc server/client packages
installed.
The factory takes a version and returns a (python_path, rpc_server_path) tuple
of the respective binaries in the virtualenv.
"""
envs = {}
def get_versioned_venv(core_version):
venv = envs.get(core_version)
if not venv:
venv = tmp_path_factory.mktemp(f"temp-{core_version}")
subprocess.check_call([sys.executable, "-m", "venv", venv])
python = find_path(venv, "python")
pkgs = [f"deltachat-rpc-server=={core_version}", f"deltachat-rpc-client=={core_version}", "pytest"]
subprocess.check_call([python, "-m", "pip", "install"] + pkgs)
envs[core_version] = venv
python = find_path(venv, "python")
rpc_server_path = find_path(venv, "deltachat-rpc-server")
print(f"python={python}\nrpc_server={rpc_server_path}")
return python, rpc_server_path
return get_versioned_venv
@pytest.fixture
def alice_and_remote_bob(tmp_path, acfactory, get_core_python_env):
"""return local Alice account, a contact to bob, and a remote 'eval' function for bob.
The 'eval' function allows to remote-execute arbitrary expressions
that can use the `bob` online account, and the `bob_contact_alice`.
"""
def factory(core_version):
python, rpc_server_path = get_core_python_env(core_version)
gw = execnet.makegateway(f"popen//python={python}")
accounts_dir = str(tmp_path.joinpath("account1_venv1"))
channel = gw.remote_exec(remote_bob_loop)
cm = os.environ.get("CHATMAIL_DOMAIN")
# trigger getting an online account on bob's side
channel.send((accounts_dir, str(rpc_server_path), cm))
# meanwhile get a local alice account
alice = acfactory.get_online_account()
channel.send(alice.self_contact.make_vcard())
# wait for bob to have started
sysinfo = channel.receive()
assert sysinfo == f"v{core_version}"
bob_vcard = channel.receive()
[alice_contact_bob] = alice.import_vcard(bob_vcard)
def eval(eval_str):
channel.send(eval_str)
return channel.receive()
return alice, alice_contact_bob, eval
return factory
def remote_bob_loop(channel):
# This function executes with versioned
# deltachat-rpc-client/server packages
# installed into the virtualenv.
#
# The "channel" argument is a send/receive pipe
# to the process that runs the corresponding remote_exec(remote_bob_loop)
import os
from deltachat_rpc_client import DeltaChat, Rpc
from deltachat_rpc_client.pytestplugin import ACFactory
accounts_dir, rpc_server_path, chatmail_domain = channel.receive()
os.environ["CHATMAIL_DOMAIN"] = chatmail_domain
# older core versions don't support specifying rpc_server_path
# so we can't just pass `rpc_server_path` argument to Rpc constructor
basepath = os.path.dirname(rpc_server_path)
os.environ["PATH"] = os.pathsep.join([basepath, os.environ["PATH"]])
rpc = Rpc(accounts_dir=accounts_dir)
with rpc:
dc = DeltaChat(rpc)
channel.send(dc.rpc.get_system_info()["deltachat_core_version"])
acfactory = ACFactory(dc)
bob = acfactory.get_online_account()
alice_vcard = channel.receive()
[alice_contact] = bob.import_vcard(alice_vcard)
ns = {"bob": bob, "bob_contact_alice": alice_contact}
channel.send(bob.self_contact.make_vcard())
while 1:
eval_str = channel.receive()
res = eval(eval_str, ns)
try:
channel.send(res)
except Exception:
# some unserializable result
channel.send(None)

View File

@@ -57,7 +57,7 @@ class Rpc:
def __init__(self, accounts_dir: Optional[str] = None, rpc_server_path="deltachat-rpc-server", **kwargs):
"""Initialize RPC client.
The given arguments will be passed to subprocess.Popen().
The 'kwargs' arguments will be passed to subprocess.Popen().
"""
if accounts_dir:
kwargs["env"] = {

View File

@@ -0,0 +1,57 @@
import subprocess
import pytest
from deltachat_rpc_client import DeltaChat, Rpc
def test_install_venv_and_use_other_core(tmp_path, get_core_python_env):
python, rpc_server_path = get_core_python_env("2.24.0")
subprocess.check_call([python, "-m", "pip", "install", "deltachat-rpc-server==2.24.0"])
rpc = Rpc(accounts_dir=tmp_path.joinpath("accounts"), rpc_server_path=rpc_server_path)
with rpc:
dc = DeltaChat(rpc)
assert dc.rpc.get_system_info()["deltachat_core_version"] == "v2.24.0"
@pytest.mark.parametrize("version", ["2.24.0"])
def test_qr_setup_contact(alice_and_remote_bob, version) -> None:
"""Test other-core Bob profile can do securejoin with Alice on current core."""
alice, alice_contact_bob, remote_eval = alice_and_remote_bob(version)
qr_code = alice.get_qr_code()
remote_eval(f"bob.secure_join({qr_code!r})")
alice.wait_for_securejoin_inviter_success()
# Test that Alice verified Bob's profile.
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.is_verified
remote_eval("bob.wait_for_securejoin_joiner_success()")
# Test that Bob verified Alice's profile.
assert remote_eval("bob_contact_alice.get_snapshot().is_verified")
def test_send_and_receive_message(alice_and_remote_bob) -> None:
"""Test other-core Bob profile can send a message to Alice on current core."""
alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.20.0")
remote_eval("bob_contact_alice.create_chat().send_text('hello')")
msg = alice.wait_for_incoming_msg()
assert msg.get_snapshot().text == "hello"
def test_second_device(acfactory, alice_and_remote_bob) -> None:
"""Test setting up current version as a second device for old version."""
_alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.20.0")
remote_eval("locals().setdefault('future', bob._rpc.provide_backup.future(bob.id))")
qr = remote_eval("bob._rpc.get_backup_qr(bob.id)")
new_account = acfactory.get_unconfigured_account()
new_account._rpc.get_backup(new_account.id, qr)
remote_eval("locals()['future']()")
assert new_account.get_config("addr") == remote_eval("bob.get_config('addr')")

View File

@@ -1,5 +1,6 @@
import pytest
from deltachat_rpc_client import EventType
from deltachat_rpc_client.rpc import JsonRpcError
@@ -156,3 +157,122 @@ def test_reconfigure_transport(acfactory) -> None:
# Reconfiguring the transport should not reset
# the settings as if when configuring the first transport.
assert account.get_config("mvbox_move") == "1"
def test_transport_synchronization(acfactory, log) -> None:
"""Test synchronization of transports between devices."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_clone = ac1.clone()
ac1_clone.bring_online()
qr = acfactory.get_account_qr()
ac1.add_transport_from_qr(qr)
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
assert len(ac1.list_transports()) == 2
assert len(ac1_clone.list_transports()) == 2
ac1_clone.add_transport_from_qr(qr)
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
assert len(ac1.list_transports()) == 3
assert len(ac1_clone.list_transports()) == 3
log.section("ac1 clone removes second transport")
[transport1, transport2, transport3] = ac1_clone.list_transports()
addr3 = transport3["addr"]
ac1_clone.delete_transport(transport2["addr"])
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
[transport1, transport3] = ac1.list_transports()
log.section("ac1 changes the primary transport")
ac1.set_config("configured_addr", transport3["addr"])
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
[transport1, transport3] = ac1_clone.list_transports()
assert ac1_clone.get_config("configured_addr") == addr3
log.section("ac1 removes the first transport")
ac1.delete_transport(transport1["addr"])
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
[transport3] = ac1_clone.list_transports()
assert transport3["addr"] == addr3
assert ac1_clone.get_config("configured_addr") == addr3
ac2_chat = ac2.create_chat(ac1)
ac2_chat.send_text("Hello!")
assert ac1.wait_for_incoming_msg().get_snapshot().text == "Hello!"
assert ac1_clone.wait_for_incoming_msg().get_snapshot().text == "Hello!"
def test_recognize_self_address(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob_chat = bob.create_chat(alice)
qr = acfactory.get_account_qr()
alice.add_transport_from_qr(qr)
new_alice_addr = alice.list_transports()[1]["addr"]
alice.set_config("configured_addr", new_alice_addr)
bob_chat.send_text("Hello!")
msg = alice.wait_for_incoming_msg().get_snapshot()
assert msg.chat == alice.create_chat(bob)
def test_transport_limit(acfactory) -> None:
"""Test transports limit."""
account = acfactory.get_online_account()
qr = acfactory.get_account_qr()
limit = 5
for _ in range(1, limit):
account.add_transport_from_qr(qr)
assert len(account.list_transports()) == limit
with pytest.raises(JsonRpcError):
account.add_transport_from_qr(qr)
second_addr = account.list_transports()[1]["addr"]
account.delete_transport(second_addr)
# test that adding a transport after deleting one works again
account.add_transport_from_qr(qr)
def test_message_info_imap_urls(acfactory, log) -> None:
"""Test that message info contains IMAP URLs of where the message was received."""
alice, bob = acfactory.get_online_accounts(2)
log.section("Alice adds ac1 clone removes second transport")
qr = acfactory.get_account_qr()
for i in range(3):
alice.add_transport_from_qr(qr)
# Wait for all transports to go IDLE after adding each one.
for _ in range(i + 1):
alice.bring_online()
new_alice_addr = alice.list_transports()[2]["addr"]
alice.set_config("configured_addr", new_alice_addr)
# Enable multi-device mode so messages are not deleted immediately.
alice.set_config("bcc_self", "1")
# Bob creates chat, learning about Alice's currently selected transport.
# This is where he will send the message.
bob_chat = bob.create_chat(alice)
# Alice changes the transport again.
alice.set_config("configured_addr", alice.list_transports()[3]["addr"])
bob_chat.send_text("Hello!")
msg = alice.wait_for_incoming_msg()
for alice_transport in alice.list_transports():
addr = alice_transport["addr"]
assert (addr == new_alice_addr) == (addr in msg.get_info())

View File

@@ -1,20 +0,0 @@
import subprocess
import sys
from platform import system # noqa
import pytest
from deltachat_rpc_client import DeltaChat, Rpc
@pytest.mark.skipif("system() == 'Windows'")
def test_install_venv_and_use_other_core(tmp_path):
venv = tmp_path.joinpath("venv1")
subprocess.check_call([sys.executable, "-m", "venv", venv])
python = venv / "bin" / "python"
subprocess.check_call([python, "-m", "pip", "install", "deltachat-rpc-server==2.20.0"])
rpc = Rpc(accounts_dir=tmp_path.joinpath("accounts"), rpc_server_path=venv.joinpath("bin", "deltachat-rpc-server"))
with rpc:
dc = DeltaChat(rpc)
assert dc.rpc.get_system_info()["deltachat_core_version"] == "v2.20.0"

View File

@@ -90,12 +90,9 @@ def test_lowercase_address(acfactory) -> None:
assert account.get_config("configured_addr") == addr
assert account.list_transports()[0]["addr"] == addr
for param in [
account.get_info()["used_account_settings"],
account.get_info()["entered_account_settings"],
]:
assert addr in param
assert addr_upper not in param
param = account.get_info()["used_transport_settings"]
assert addr in param
assert addr_upper not in param
def test_configure_ip(acfactory) -> None:
@@ -343,9 +340,11 @@ def test_receive_imf_failure(acfactory) -> None:
msg_id = event.msg_id
message = bob.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
version = bob.get_info()["deltachat_core_version"]
assert (
snapshot.text == "❌ Failed to receive a message:"
" Condition failed: `!context.get_config_bool(Config::FailOnReceivingFullMsg).await?`."
f" Core version {version}."
" Please report this bug to delta@merlinux.eu or https://support.delta.chat/."
)
@@ -508,6 +507,103 @@ def test_import_export_backup(acfactory, tmp_path) -> None:
assert alice2.manager.get_system_info()
def test_import_export_online_all(acfactory, tmp_path, data, log) -> None:
(ac1, some1) = acfactory.get_online_accounts(2)
log.section("create some chat content")
some1_addr = some1.get_config("addr")
chat1 = ac1.create_contact(some1).create_chat()
chat1.send_text("msg1")
assert len(ac1.get_contacts()) == 1
original_image_path = data.get_path("image/avatar64x64.png")
chat1.send_file(str(original_image_path))
# Add another 100KB file that ensures that the progress is smooth enough
path = tmp_path / "attachment.txt"
with path.open("w") as file:
file.truncate(100000)
chat1.send_file(str(path))
def assert_account_is_proper(ac):
contacts = ac.get_contacts()
assert len(contacts) == 1
contact2 = contacts[0]
assert contact2.get_snapshot().address == some1_addr
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 3 + E2EE_INFO_MSGS
assert messages[0 + E2EE_INFO_MSGS].get_snapshot().text == "msg1"
snapshot = messages[1 + E2EE_INFO_MSGS].get_snapshot()
assert snapshot.file_mime == "image/png"
assert os.stat(snapshot.file).st_size == os.stat(original_image_path).st_size
ac.set_config("displayname", "new displayname")
assert ac.get_config("displayname") == "new displayname"
assert_account_is_proper(ac1)
backupdir = tmp_path / "backup"
backupdir.mkdir()
log.section(f"export all to {backupdir}")
ac1.stop_io()
ac1.export_backup(backupdir)
progress = 0
files_written = []
while True:
event = ac1.wait_for_event()
if event.kind == EventType.IMEX_PROGRESS:
assert event.progress > 0 # Progress 0 indicates error.
assert event.progress < progress + 250
progress = event.progress
if progress == 1000:
break
elif event.kind == EventType.IMEX_FILE_WRITTEN:
files_written.append(event.path)
else:
logging.info(event)
assert len(files_written) == 1
assert os.path.exists(files_written[0])
ac1.start_io()
log.section("get fresh empty account")
ac2 = acfactory.get_unconfigured_account()
log.section("import backup and check it's proper")
ac2.import_backup(files_written[0])
progress = 0
while True:
event = ac2.wait_for_event()
if event.kind == EventType.IMEX_PROGRESS:
assert event.progress > 0 # Progress 0 indicates error.
assert event.progress < progress + 250
progress = event.progress
if progress == 1000:
break
else:
logging.info(event)
assert_account_is_proper(ac1)
assert_account_is_proper(ac2)
log.section(f"Second-time export all to {backupdir}")
ac1.stop_io()
ac1.export_backup(backupdir)
while True:
event = ac1.wait_for_event()
if event.kind == EventType.IMEX_PROGRESS:
assert event.progress > 0
if event.progress == 1000:
break
elif event.kind == EventType.IMEX_FILE_WRITTEN:
files_written.append(event.path)
else:
logging.info(event)
assert len(files_written) == 2
assert os.path.exists(files_written[1])
assert files_written[1] != files_written[0]
assert len(list(backupdir.glob("*.tar"))) == 2
def test_import_export_keys(acfactory, tmp_path) -> None:
alice, bob = acfactory.get_online_accounts(2)
@@ -733,7 +829,7 @@ def test_configured_imap_certificate_checks(acfactory):
alice = acfactory.new_configured_account()
# Certificate checks should be configured (not None)
assert "cert_strict" in alice.get_info().used_account_settings
assert "cert_strict" in alice.get_info().used_transport_settings
# "cert_old_automatic" is the value old Delta Chat core versions used
# to mean user entered "imap_certificate_checks=0" (Automatic)
@@ -746,7 +842,7 @@ def test_configured_imap_certificate_checks(acfactory):
#
# Core 1.142.4, 1.142.5 and 1.142.6 saved this value due to bug.
# This test is a regression test to prevent this happening again.
assert "cert_old_automatic" not in alice.get_info().used_account_settings
assert "cert_old_automatic" not in alice.get_info().used_transport_settings
def test_no_old_msg_is_fresh(acfactory):
@@ -1012,3 +1108,47 @@ def test_message_exists(acfactory):
ac1.remove()
assert not message1.exists()
assert not message2.exists()
def test_synchronize_member_list_on_group_rejoin(acfactory, log):
"""
Test that user recreates group member list when it joins the group again.
ac1 creates a group with two other accounts: ac2 and ac3
Then it removes ac2, removes ac3 and adds ac2 back.
ac2 did not see that ac3 is removed, so it should rebuild member list from scratch.
"""
log.section("setting up accounts, accepted with each other")
ac1, ac2, ac3 = accounts = acfactory.get_online_accounts(3)
log.section("ac1: creating group chat with 2 other members")
chat = ac1.create_group("title1")
chat.add_contact(ac2)
chat.add_contact(ac3)
log.section("ac1: send message to new group chat")
msg = chat.send_text("hello")
assert chat.num_contacts() == 3
log.section("checking that the chat arrived correctly")
for ac in accounts[1:]:
msg = ac.wait_for_incoming_msg().get_snapshot()
assert msg.text == "hello"
assert msg.chat.num_contacts() == 3
msg.chat.accept()
log.section("ac1: removing ac2")
chat.remove_contact(ac2)
log.section("ac2: wait for a message about removal from the chat")
ac2.wait_for_incoming_msg()
log.section("ac1: removing ac3")
chat.remove_contact(ac3)
log.section("ac1: adding ac2 back")
chat.add_contact(ac2)
log.section("ac2: check that ac3 is removed")
msg = ac2.wait_for_incoming_msg()
assert chat.num_contacts() == 2
assert msg.get_snapshot().chat.num_contacts() == 2

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "2.29.0"
version = "2.36.0"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"

View File

@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "2.29.0"
"version": "2.36.0"
}

View File

@@ -43,7 +43,7 @@ async fn main_impl() -> Result<()> {
if let Some(arg) = args.next() {
return Err(anyhow!("Unrecognized argument {arg:?}"));
}
eprintln!("{}", &*DC_VERSION_STR);
eprintln!("{DC_VERSION_STR}");
return Ok(());
} else if first_arg.to_str() == Some("--openrpc") {
if let Some(arg) = args.next() {

View File

@@ -12,6 +12,11 @@ ignore = [
# Unmaintained paste
"RUSTSEC-2024-0436",
# Unmaintained rustls-pemfile
# 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"
]
[bans]

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "2.29.0"
version = "2.36.0"
license = "MPL-2.0"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"

View File

@@ -1,4 +1,3 @@
import sys
import time
import deltachat as dc
@@ -63,56 +62,6 @@ class TestGroupStressTests:
# Message should be encrypted because keys of other members are gossiped
assert msg.is_encrypted()
def test_synchronize_member_list_on_group_rejoin(self, acfactory, lp):
"""
Test that user recreates group member list when it joins the group again.
ac1 creates a group with two other accounts: ac2 and ac3
Then it removes ac2, removes ac3 and adds ac2 back.
ac2 did not see that ac3 is removed, so it should rebuild member list from scratch.
"""
lp.sec("setting up accounts, accepted with each other")
accounts = acfactory.get_online_accounts(3)
acfactory.introduce_each_other(accounts)
ac1, ac2, ac3 = accounts
lp.sec("ac1: creating group chat with 2 other members")
chat = ac1.create_group_chat("title1", contacts=[ac2, ac3])
assert not chat.is_promoted()
lp.sec("ac1: send message to new group chat")
msg = chat.send_text("hello")
assert chat.is_promoted() and msg.is_encrypted()
assert chat.num_contacts() == 3
lp.sec("checking that the chat arrived correctly")
for ac in accounts[1:]:
msg = ac._evtracker.wait_next_incoming_message()
assert msg.text == "hello"
print("chat is", msg.chat)
assert msg.chat.num_contacts() == 3
lp.sec("ac1: removing ac2")
chat.remove_contact(ac2)
lp.sec("ac2: wait for a message about removal from the chat")
msg = ac2._evtracker.wait_next_incoming_message()
lp.sec("ac1: removing ac3")
chat.remove_contact(ac3)
lp.sec("ac1: adding ac2 back")
# Group is promoted, message is sent automatically
assert chat.is_promoted()
chat.add_contact(ac2)
lp.sec("ac2: check that ac3 is removed")
msg = ac2._evtracker.wait_next_incoming_message()
assert chat.num_contacts() == 2
assert msg.chat.num_contacts() == 2
acfactory.dump_imap_summary(sys.stdout)
def test_qr_verified_group_and_chatting(acfactory, lp):
ac1, ac2, ac3 = acfactory.get_online_accounts(3)

View File

@@ -9,7 +9,6 @@ from imap_tools import AND
import deltachat as dc
from deltachat import account_hookimpl, Message
from deltachat.tracker import ImexTracker
from deltachat.testplugin import E2EE_INFO_MSGS
@@ -269,22 +268,23 @@ def test_enable_mvbox_move(acfactory, lp):
assert ac2._evtracker.wait_next_incoming_message().text == "message1"
def test_move_sync_msgs(acfactory):
def test_dont_move_sync_msgs(acfactory):
ac1 = acfactory.new_online_configuring_account(bcc_self=True, sync_msgs=True, fix_is_chatmail=True)
acfactory.bring_accounts_online()
ac1.direct_imap.select_folder("DeltaChat")
ac1.direct_imap.select_folder("Inbox")
# Sync messages may also be sent during the configuration.
mvbox_msg_cnt = len(ac1.direct_imap.get_all_messages())
inbox_msg_cnt = len(ac1.direct_imap.get_all_messages())
ac1.set_config("displayname", "Alice")
ac1._evtracker.get_matching("DC_EVENT_MSG_DELIVERED")
ac1.set_config("displayname", "Bob")
ac1._evtracker.get_matching("DC_EVENT_MSG_DELIVERED")
ac1.direct_imap.select_folder("Inbox")
assert len(ac1.direct_imap.get_all_messages()) == 0
assert len(ac1.direct_imap.get_all_messages()) == inbox_msg_cnt + 2
ac1.direct_imap.select_folder("DeltaChat")
assert len(ac1.direct_imap.get_all_messages()) == mvbox_msg_cnt + 2
assert len(ac1.direct_imap.get_all_messages()) == 0
def test_forward_messages(acfactory, lp):
@@ -826,86 +826,6 @@ def test_send_and_receive_image(acfactory, lp, data):
assert m == msg_in
def test_import_export_online_all(acfactory, tmp_path, data, lp):
(ac1, some1) = acfactory.get_online_accounts(2)
lp.sec("create some chat content")
some1_addr = some1.get_config("addr")
chat1 = ac1.create_contact(some1).create_chat()
chat1.send_text("msg1")
assert len(ac1.get_contacts()) == 1
original_image_path = data.get_path("d.png")
chat1.send_image(original_image_path)
# Add another 100KB file that ensures that the progress is smooth enough
path = tmp_path / "attachment.txt"
with path.open("w") as file:
file.truncate(100000)
chat1.send_file(str(path))
def assert_account_is_proper(ac):
contacts = ac.get_contacts()
assert len(contacts) == 1
contact2 = contacts[0]
assert contact2.addr == some1_addr
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 3 + E2EE_INFO_MSGS
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
assert messages[1 + E2EE_INFO_MSGS].filemime == "image/png"
assert os.stat(messages[1 + E2EE_INFO_MSGS].filename).st_size == os.stat(original_image_path).st_size
ac.set_config("displayname", "new displayname")
assert ac.get_config("displayname") == "new displayname"
assert_account_is_proper(ac1)
backupdir = tmp_path / "backup"
backupdir.mkdir()
lp.sec(f"export all to {backupdir}")
with ac1.temp_plugin(ImexTracker()) as imex_tracker:
ac1.stop_io()
ac1.imex(str(backupdir), dc.const.DC_IMEX_EXPORT_BACKUP)
# check progress events for export
assert imex_tracker.wait_progress(1, progress_upper_limit=249)
assert imex_tracker.wait_progress(250, progress_upper_limit=499)
assert imex_tracker.wait_progress(500, progress_upper_limit=749)
assert imex_tracker.wait_progress(750, progress_upper_limit=999)
paths = imex_tracker.wait_finish()
assert len(paths) == 1
path = paths[0]
assert os.path.exists(path)
ac1.start_io()
lp.sec("get fresh empty account")
ac2 = acfactory.get_unconfigured_account()
lp.sec("get latest backup file")
path2 = ac2.get_latest_backupfile(str(backupdir))
assert path2 == path
lp.sec("import backup and check it's proper")
with ac2.temp_plugin(ImexTracker()) as imex_tracker:
ac2.import_all(path)
# check progress events for import
assert imex_tracker.wait_progress(1, progress_upper_limit=249)
assert imex_tracker.wait_progress(1000)
assert_account_is_proper(ac1)
assert_account_is_proper(ac2)
lp.sec(f"Second-time export all to {backupdir}")
ac1.stop_io()
path2 = ac1.export_all(str(backupdir))
assert os.path.exists(path2)
assert path2 != path
assert ac2.get_latest_backupfile(str(backupdir)) == path2
def test_qr_email_capitalization(acfactory, lp):
"""Regression test for a bug
that resulted in failure to propagate verification
@@ -1295,16 +1215,17 @@ def test_configure_error_msgs_invalid_server(acfactory):
ev = ac2._evtracker.get_matching("DC_EVENT_CONFIGURE_PROGRESS")
if ev.data1 == 0:
break
err_lower = ev.data2.lower()
# Can't connect so it probably should say something about "internet"
# again, should not repeat itself
# If this fails then probably `e.msg.to_lowercase().contains("could not resolve")`
# in configure.rs returned false because the error message was changed
# (i.e. did not contain "could not resolve" anymore)
assert (ev.data2.count("internet") + ev.data2.count("network")) == 1
assert (err_lower.count("internet") + err_lower.count("network")) == 1
# Should mention that it can't connect:
assert ev.data2.count("connect") == 1
assert err_lower.count("connect") == 1
# The users do not know what "configuration" is
assert "configuration" not in ev.data2.lower()
assert "configuration" not in err_lower
def test_status(acfactory):

View File

@@ -1 +1 @@
2025-12-01
2026-01-03

View File

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

View File

@@ -377,6 +377,11 @@ impl Accounts {
"Starting background fetch for {n_accounts} accounts."
)),
});
::tracing::event!(
::tracing::Level::INFO,
account_id = 0,
"Starting background fetch for {n_accounts} accounts."
);
let mut set = JoinSet::new();
for account in accounts {
set.spawn(async move {
@@ -392,6 +397,11 @@ impl Accounts {
"Finished background fetch for {n_accounts} accounts."
)),
});
::tracing::event!(
::tracing::Level::INFO,
account_id = 0,
"Finished background fetch for {n_accounts} accounts."
);
}
/// Auxiliary function for [Accounts::background_fetch].
@@ -429,6 +439,11 @@ impl Accounts {
id: 0,
typ: EventType::Warning("Background fetch timed out.".to_string()),
});
::tracing::event!(
::tracing::Level::WARN,
account_id = 0,
"Background fetch timed out."
);
}
events.emit(Event {
id: 0,

View File

@@ -6,15 +6,15 @@ use crate::chat::ChatIdBlocked;
use crate::chat::{Chat, ChatId, send_msg};
use crate::constants::{Blocked, Chattype};
use crate::contact::ContactId;
use crate::context::Context;
use crate::context::{Context, WeakContext};
use crate::events::EventType;
use crate::headerdef::HeaderDef;
use crate::log::warn;
use crate::message::{self, Message, MsgId, Viewtype};
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::tools::time;
use crate::tools::{normalize_text, time};
use anyhow::{Context as _, Result, ensure};
use sdp::SessionDescription;
use serde::Serialize;
@@ -33,7 +33,7 @@ use tokio::time::sleep;
///
/// For the caller, this means they should also not wait longer,
/// as the callee won't start the call afterwards.
const RINGING_SECONDS: i64 = 60;
const RINGING_SECONDS: i64 = 120;
// For persisting parameters in the call, we use Param::Arg*
@@ -86,7 +86,7 @@ impl CallInfo {
.sql
.execute(
"UPDATE msgs SET txt=?, txt_normalized=? WHERE id=?",
(text, message::normalize_text(text), self.msg.id),
(text, normalize_text(text), self.msg.id),
)
.await?;
Ok(())
@@ -199,8 +199,9 @@ impl Context {
call.id = send_msg(self, chat_id, &mut call).await?;
let wait = RINGING_SECONDS;
let context = self.get_weak_context();
task::spawn(Context::emit_end_call_if_unaccepted(
self.clone(),
context,
wait.try_into()?,
call.id,
));
@@ -291,11 +292,12 @@ impl Context {
}
async fn emit_end_call_if_unaccepted(
context: Context,
context: WeakContext,
wait: u64,
call_id: MsgId,
) -> Result<()> {
sleep(Duration::from_secs(wait)).await;
let context = context.upgrade()?;
let Some(mut call) = context.load_call_by_id(call_id).await? else {
warn!(
context,
@@ -368,8 +370,9 @@ impl Context {
}
}
let wait = call.remaining_ring_seconds();
let context = self.get_weak_context();
task::spawn(Context::emit_end_call_if_unaccepted(
self.clone(),
context,
wait.try_into()?,
call.msg.id,
));
@@ -660,9 +663,7 @@ pub(crate) async fn create_fallback_ice_servers(context: &Context) -> Result<Str
// because of bandwidth costs:
// <https://github.com/jselbie/stunserver/issues/50>
// We use nine.testrun.org for a default STUN server.
let hostname = "nine.testrun.org";
// Do not use cache because there is no TLS.
let load_cache = false;
let urls: Vec<String> = lookup_host_with_cache(context, hostname, STUN_PORT, "", load_cache)
@@ -670,14 +671,27 @@ pub(crate) async fn create_fallback_ice_servers(context: &Context) -> Result<Str
.into_iter()
.map(|addr| format!("stun:{addr}"))
.collect();
let ice_server = IceServer {
let stun_server = IceServer {
urls,
username: None,
credential: None,
};
let json = serde_json::to_string(&[ice_server])?;
let hostname = "turn.delta.chat";
// Do not use cache because there is no TLS.
let load_cache = false;
let urls: Vec<String> = lookup_host_with_cache(context, hostname, STUN_PORT, "", load_cache)
.await?
.into_iter()
.map(|addr| format!("turn:{addr}"))
.collect();
let turn_server = IceServer {
urls,
username: Some("public".to_string()),
credential: Some("o4tR7yG4rG2slhXqRUf9zgmHz".to_string()),
};
let json = serde_json::to_string(&[stun_server, turn_server])?;
Ok(json)
}

View File

@@ -45,7 +45,7 @@ use crate::sync::{self, Sync::*, SyncData};
use crate::tools::{
IsNoneOrEmpty, SystemTime, buf_compress, create_broadcast_secret, create_id,
create_outgoing_rfc724_mid, create_smeared_timestamp, create_smeared_timestamps, get_abs_path,
gm2local_offset, smeared_time, time, truncate_msg_text,
gm2local_offset, normalize_text, smeared_time, time, truncate_msg_text,
};
use crate::webxdc::StatusUpdateSerial;
use crate::{chatlist_events, imap};
@@ -286,10 +286,11 @@ impl ChatId {
let timestamp = cmp::min(timestamp, smeared_time(context));
let row_id =
context.sql.insert(
"INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected, param) VALUES(?, ?, ?, ?, ?, 0, ?);",
"INSERT INTO chats (type, name, name_normalized, grpid, blocked, created_timestamp, protected, param) VALUES(?, ?, ?, ?, ?, ?, 0, ?)",
(
chattype,
&grpname,
normalize_text(&grpname),
grpid,
create_blocked,
timestamp,
@@ -431,14 +432,18 @@ impl ChatId {
match chat.typ {
Chattype::Single | Chattype::Group | Chattype::OutBroadcast | Chattype::InBroadcast => {
// User has "created a chat" with all these contacts.
//
// Previously accepting a chat literally created a chat because unaccepted chats
// went to "contact requests" list rather than normal chatlist.
// But for groups we use lower origin because users don't always check all members
// before accepting a chat and may not want to have the group members mixed with
// existing contacts. `IncomingTo` fits here by its definition.
let origin = match chat.typ {
Chattype::Group => Origin::IncomingTo,
_ => Origin::CreateChat,
};
for contact_id in get_chat_contacts(context, self).await? {
if contact_id != ContactId::SELF {
ContactId::scaleup_origin(context, &[contact_id], Origin::CreateChat)
.await?;
ContactId::scaleup_origin(context, &[contact_id], origin).await?;
}
}
}
@@ -595,6 +600,10 @@ impl ChatId {
}
/// Deletes a chat.
///
/// Messages are deleted from the device and the chat database entry is deleted.
/// After that, a `MsgsChanged` event is emitted.
/// Messages are deleted from the server in background.
pub async fn delete(self, context: &Context) -> Result<()> {
self.delete_ex(context, Sync).await
}
@@ -653,7 +662,7 @@ impl ChatId {
context
.set_config_internal(Config::LastHousekeeping, None)
.await?;
context.scheduler.interrupt_inbox().await;
context.scheduler.interrupt_smtp().await;
Ok(())
}
@@ -782,7 +791,7 @@ impl ChatId {
time(),
msg.viewtype,
&msg.text,
message::normalize_text(&msg.text),
normalize_text(&msg.text),
msg.param.to_string(),
msg.in_reply_to.as_deref().unwrap_or_default(),
msg.id,
@@ -823,7 +832,7 @@ impl ChatId {
msg.viewtype,
MessageState::OutDraft,
&msg.text,
message::normalize_text(&msg.text),
normalize_text(&msg.text),
msg.param.to_string(),
1,
msg.in_reply_to.as_deref().unwrap_or_default(),
@@ -1919,7 +1928,7 @@ impl Chat {
msg.viewtype,
msg.state,
msg_text,
message::normalize_text(&msg_text),
normalize_text(&msg_text),
&msg.subject,
msg.param.to_string(),
msg.hidden,
@@ -1970,7 +1979,7 @@ impl Chat {
msg.viewtype,
msg.state,
msg_text,
message::normalize_text(&msg_text),
normalize_text(&msg_text),
&msg.subject,
msg.param.to_string(),
msg.hidden,
@@ -2106,7 +2115,7 @@ pub(crate) async fn sync(context: &Context, id: SyncId, action: SyncAction) -> R
context
.add_sync_item(SyncData::AlterChat { id, action })
.await?;
context.scheduler.interrupt_inbox().await;
context.scheduler.interrupt_smtp().await;
Ok(())
}
@@ -2274,8 +2283,8 @@ async fn update_special_chat_name(
context
.sql
.execute(
"UPDATE chats SET name=? WHERE id=? AND name!=?",
(&name, chat_id, &name),
"UPDATE chats SET name=?, name_normalized=? WHERE id=? AND name!=?",
(&name, normalize_text(&name), chat_id, &name),
)
.await?;
}
@@ -2388,11 +2397,12 @@ impl ChatIdBlocked {
.transaction(move |transaction| {
transaction.execute(
"INSERT INTO chats
(type, name, param, blocked, created_timestamp)
VALUES(?, ?, ?, ?, ?)",
(type, name, name_normalized, param, blocked, created_timestamp)
VALUES(?, ?, ?, ?, ?, ?)",
(
Chattype::Single,
chat_name,
&chat_name,
normalize_text(&chat_name),
params.to_string(),
create_blocked as u8,
smeared_time,
@@ -2726,7 +2736,7 @@ async fn prepare_send_msg(
Ok(row_ids)
}
/// Constructs jobs for sending a message and inserts them into the appropriate table.
/// Constructs jobs for sending a message and inserts them into the `smtp` table.
///
/// Updates the message `GuaranteeE2ee` parameter and persists it
/// in the database depending on whether the message
@@ -2850,30 +2860,27 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
let chunk_size = context.get_max_smtp_rcpt_to().await?;
let trans_fn = |t: &mut rusqlite::Transaction| {
let mut row_ids = Vec::<i64>::new();
if let Some(sync_ids) = rendered_msg.sync_ids_to_delete {
t.execute(
&format!("DELETE FROM multi_device_sync WHERE id IN ({sync_ids})"),
(),
)?;
t.execute(
"INSERT INTO imap_send (mime, msg_id) VALUES (?, ?)",
(&rendered_msg.message, msg.id),
}
for recipients_chunk in recipients.chunks(chunk_size) {
let recipients_chunk = recipients_chunk.join(" ");
let row_id = t.execute(
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id) \
VALUES (?1, ?2, ?3, ?4)",
(
&rendered_msg.rfc724_mid,
recipients_chunk,
&rendered_msg.message,
msg.id,
),
)?;
} else {
for recipients_chunk in recipients.chunks(chunk_size) {
let recipients_chunk = recipients_chunk.join(" ");
let row_id = t.execute(
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id) \
VALUES (?1, ?2, ?3, ?4)",
(
&rendered_msg.rfc724_mid,
recipients_chunk,
&rendered_msg.message,
msg.id,
),
)?;
row_ids.push(row_id.try_into()?);
}
row_ids.push(row_id.try_into()?);
}
Ok(row_ids)
};
@@ -2944,7 +2951,7 @@ pub(crate) async fn save_text_edit_to_db(
"UPDATE msgs SET txt=?, txt_normalized=?, param=? WHERE id=?",
(
new_text,
message::normalize_text(new_text),
normalize_text(new_text),
original_msg.param.to_string(),
original_msg.id,
),
@@ -3088,7 +3095,7 @@ pub async fn get_chat_msgs_ex(
WHERE m.chat_id=?
AND m.hidden=0
AND (
m.param GLOB \"*S=*\"
m.param GLOB '*\nS=*' OR param GLOB 'S=*'
OR m.from_id == ?
OR m.to_id == ?
);",
@@ -3433,9 +3440,15 @@ pub(crate) async fn create_group_ex(
.sql
.insert(
"INSERT INTO chats
(type, name, grpid, param, created_timestamp)
VALUES(?, ?, ?, \'U=1\', ?);",
(Chattype::Group, &chat_name, &grpid, timestamp),
(type, name, name_normalized, grpid, param, created_timestamp)
VALUES(?, ?, ?, ?, \'U=1\', ?)",
(
Chattype::Group,
&chat_name,
normalize_text(&chat_name),
&grpid,
timestamp,
),
)
.await?;
@@ -3519,9 +3532,15 @@ pub(crate) async fn create_out_broadcast_ex(
t.execute(
"INSERT INTO chats
(type, name, grpid, created_timestamp)
VALUES(?, ?, ?, ?);",
(Chattype::OutBroadcast, &chat_name, &grpid, timestamp),
(type, name, name_normalized, grpid, created_timestamp)
VALUES(?, ?, ?, ?, ?)",
(
Chattype::OutBroadcast,
&chat_name,
normalize_text(&chat_name),
&grpid,
timestamp,
),
)?;
let chat_id = ChatId::new(t.last_insert_rowid().try_into()?);
@@ -3823,7 +3842,7 @@ pub(crate) async fn add_contact_to_chat_ex(
.log_err(context)
.is_ok()
{
context.scheduler.interrupt_inbox().await;
context.scheduler.interrupt_smtp().await;
}
}
context.emit_event(EventType::ChatModified(chat_id));
@@ -4094,8 +4113,8 @@ async fn rename_ex(
context
.sql
.execute(
"UPDATE chats SET name=? WHERE id=?;",
(new_name.to_string(), chat_id),
"UPDATE chats SET name=?, name_normalized=? WHERE id=?",
(&new_name, normalize_text(&new_name), chat_id),
)
.await?;
if chat.is_promoted()
@@ -4189,6 +4208,16 @@ pub async fn set_chat_profile_image(
/// Forwards multiple messages to a chat.
pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId) -> Result<()> {
forward_msgs_2ctx(context, msg_ids, context, chat_id).await
}
/// Forwards multiple messages to a chat in another context.
pub async fn forward_msgs_2ctx(
ctx_src: &Context,
msg_ids: &[MsgId],
ctx_dst: &Context,
chat_id: ChatId,
) -> Result<()> {
ensure!(!msg_ids.is_empty(), "empty msgs_ids: nothing to forward");
ensure!(!chat_id.is_special(), "can not forward to special chat");
@@ -4196,16 +4225,16 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
let mut curr_timestamp: i64;
chat_id
.unarchive_if_not_muted(context, MessageState::Undefined)
.unarchive_if_not_muted(ctx_dst, MessageState::Undefined)
.await?;
let mut chat = Chat::load_from_db(context, chat_id).await?;
if let Some(reason) = chat.why_cant_send(context).await? {
let mut chat = Chat::load_from_db(ctx_dst, chat_id).await?;
if let Some(reason) = chat.why_cant_send(ctx_dst).await? {
bail!("cannot send to {chat_id}: {reason}");
}
curr_timestamp = create_smeared_timestamps(context, msg_ids.len());
curr_timestamp = create_smeared_timestamps(ctx_dst, msg_ids.len());
let mut msgs = Vec::with_capacity(msg_ids.len());
for id in msg_ids {
let ts: i64 = context
let ts: i64 = ctx_src
.sql
.query_get_value("SELECT timestamp FROM msgs WHERE id=?", (id,))
.await?
@@ -4215,11 +4244,14 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
msgs.sort_unstable();
for (_, id) in msgs {
let src_msg_id: MsgId = id;
let mut msg = Message::load_from_db(context, src_msg_id).await?;
let mut msg = Message::load_from_db(ctx_src, src_msg_id).await?;
if msg.state == MessageState::OutDraft {
bail!("cannot forward drafts.");
}
let mut param = msg.param;
msg.param = Params::new();
if msg.get_viewtype() != Viewtype::Sticker {
msg.param
.set_int(Param::Forwarded, src_msg_id.to_u32() as i32);
@@ -4229,17 +4261,16 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
msg.viewtype = Viewtype::Text;
}
msg.param.remove(Param::GuaranteeE2ee);
msg.param.remove(Param::ForcePlaintext);
msg.param.remove(Param::Cmd);
msg.param.remove(Param::OverrideSenderDisplayname);
msg.param.remove(Param::WebxdcDocument);
msg.param.remove(Param::WebxdcDocumentTimestamp);
msg.param.remove(Param::WebxdcSummary);
msg.param.remove(Param::WebxdcSummaryTimestamp);
msg.param.remove(Param::IsEdited);
msg.param.remove(Param::WebrtcRoom);
msg.param.remove(Param::WebrtcAccepted);
let param = &mut param;
msg.param.steal(param, Param::File);
msg.param.steal(param, Param::Filename);
msg.param.steal(param, Param::Width);
msg.param.steal(param, Param::Height);
msg.param.steal(param, Param::Duration);
msg.param.steal(param, Param::MimeType);
msg.param.steal(param, Param::ProtectQuote);
msg.param.steal(param, Param::Quote);
msg.param.steal(param, Param::Summary1);
msg.in_reply_to = None;
// do not leak data as group names; a default subject is generated by mimefactory
@@ -4248,16 +4279,16 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
msg.state = MessageState::OutPending;
msg.rfc724_mid = create_outgoing_rfc724_mid();
msg.timestamp_sort = curr_timestamp;
chat.prepare_msg_raw(context, &mut msg, None).await?;
chat.prepare_msg_raw(ctx_dst, &mut msg, None).await?;
curr_timestamp += 1;
if !create_send_msg_jobs(context, &mut msg).await?.is_empty() {
context.scheduler.interrupt_smtp().await;
if !create_send_msg_jobs(ctx_dst, &mut msg).await?.is_empty() {
ctx_dst.scheduler.interrupt_smtp().await;
}
created_msgs.push(msg.id);
}
for msg_id in created_msgs {
context.emit_msgs_changed(chat_id, msg_id);
ctx_dst.emit_msgs_changed(chat_id, msg_id);
}
Ok(())
}
@@ -4285,7 +4316,7 @@ pub async fn save_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
})
.await?;
}
context.scheduler.interrupt_inbox().await;
context.scheduler.interrupt_smtp().await;
Ok(())
}
@@ -4529,7 +4560,7 @@ pub async fn add_device_msg_with_importance(
msg.viewtype,
state,
&msg.text,
message::normalize_text(&msg.text),
normalize_text(&msg.text),
msg.param.to_string(),
rfc724_mid,
),
@@ -4668,7 +4699,7 @@ pub(crate) async fn add_info_msg_with_cmd(
Viewtype::Text,
MessageState::InNoticed,
text,
message::normalize_text(text),
normalize_text(text),
rfc724_mid,
ephemeral_timer,
param.to_string(),
@@ -4710,7 +4741,7 @@ pub(crate) async fn update_msg_text_and_timestamp(
.sql
.execute(
"UPDATE msgs SET txt=?, txt_normalized=?, timestamp=? WHERE id=?;",
(text, message::normalize_text(text), timestamp, msg_id),
(text, normalize_text(text), timestamp, msg_id),
)
.await?;
context.emit_msgs_changed(chat_id, msg_id);

View File

@@ -5240,6 +5240,44 @@ async fn test_send_delete_request_no_encryption() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_forward_msgs_2ctx() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_chat = alice.create_chat(bob).await;
let alice_sent = alice.send_text(alice_chat.id, "hi").await;
let bob_alice_msg = bob.recv_msg(&alice_sent).await;
let bob_chat_id = bob_alice_msg.chat_id;
bob_chat_id.accept(bob).await?;
let bob_text = "Hi, did you know we're using the same device so i have access to your profile?";
let bob_sent = bob.send_text(bob_chat_id, bob_text).await;
alice.recv_msg(&bob_sent).await;
let alice_chat_len = alice_chat.id.get_msg_cnt(alice).await?;
forward_msgs_2ctx(
bob,
&[bob_alice_msg.id, bob_sent.sender_msg_id],
alice,
alice_chat.id,
)
.await?;
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, alice_chat_len + 2);
let msg = alice.get_last_msg().await;
assert!(msg.is_forwarded());
assert_eq!(msg.text, bob_text);
assert_eq!(msg.from_id, ContactId::SELF);
let sent = alice.pop_sent_msg().await;
let msg = bob.recv_msg(&sent).await;
assert!(msg.is_forwarded());
assert_eq!(msg.text, bob_text);
assert_eq!(msg.from_id, bob_alice_msg.from_id);
Ok(())
}
/// Tests that in multi-device setup
/// second device learns the key of a contact
/// via Autocrypt-Gossip in 1:1 chats.

View File

@@ -185,7 +185,7 @@ impl Chatlist {
warn!(context, "Cannot update special chat names: {err:#}.")
}
let str_like_cmd = format!("%{query}%");
let str_like_cmd = format!("%{}%", query.to_lowercase());
context
.sql
.query_map_vec(
@@ -201,7 +201,7 @@ impl Chatlist {
ORDER BY timestamp DESC, id DESC LIMIT 1)
WHERE c.id>9 AND c.id!=?2
AND c.blocked!=1
AND c.name LIKE ?3
AND IFNULL(c.name_normalized,c.name) LIKE ?3
AND (NOT ?4 OR EXISTS (SELECT 1 FROM msgs m WHERE m.chat_id = c.id AND m.state == ?5 AND hidden=0))
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
@@ -472,7 +472,7 @@ mod tests {
use crate::chat::save_msgs;
use crate::chat::{
add_contact_to_chat, create_group, get_chat_contacts, remove_contact_from_chat,
send_text_msg,
send_text_msg, set_chat_name,
};
use crate::receive_imf::receive_imf;
use crate::stock_str::StockMessage;
@@ -482,7 +482,7 @@ mod tests {
use std::time::Duration;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_try_load() {
async fn test_try_load() -> Result<()> {
let mut tcm = TestContextManager::new();
let bob = &tcm.bob().await;
let chat_id1 = create_group(bob, "a chat").await.unwrap();
@@ -552,6 +552,15 @@ mod tests {
.await
.unwrap();
assert_eq!(chats.len(), 1);
let chat_id = create_group(bob, "Δ-chat").await.unwrap();
let chats = Chatlist::try_load(bob, 0, Some("δ"), None).await?;
assert_eq!(chats.len(), 1);
assert_eq!(chats.ids[0].0, chat_id);
set_chat_name(bob, chat_id, "abcδe").await?;
let chats = Chatlist::try_load(bob, 0, Some("Δ"), None).await?;
assert_eq!(chats.len(), 1);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -13,7 +13,6 @@ use strum_macros::{AsRefStr, Display, EnumIter, EnumString};
use tokio::fs;
use crate::blob::BlobObject;
use crate::configure::EnteredLoginParam;
use crate::context::Context;
use crate::events::EventType;
use crate::log::LogExt;
@@ -21,7 +20,7 @@ use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::provider::Provider;
use crate::sync::{self, Sync::*, SyncData};
use crate::tools::get_abs_path;
use crate::transport::ConfiguredLoginParam;
use crate::transport::{ConfiguredLoginParam, add_pseudo_transport, send_sync_transports};
use crate::{constants, stats};
/// The available configuration keys.
@@ -204,7 +203,7 @@ pub enum Config {
/// `ProviderOptions::delete_to_trash`.
DeleteToTrash,
/// The primary email address. Also see `SecondaryAddrs`.
/// The primary email address.
ConfiguredAddr,
/// List of configured IMAP servers as a JSON array.
@@ -306,10 +305,6 @@ pub enum Config {
/// Meant to help profile owner to differ between profiles with similar names.
PrivateTag,
/// All secondary self addresses separated by spaces
/// (`addr1@example.org addr2@example.org addr3@example.org`)
SecondaryAddrs,
/// Read-only core version string.
#[strum(serialize = "sys.version")]
SysVersion,
@@ -509,7 +504,7 @@ impl Context {
.into_owned()
})
}
Config::SysVersion => Some((*constants::DC_VERSION_STR).clone()),
Config::SysVersion => Some(constants::DC_VERSION_STR.to_string()),
Config::SysMsgsizeMaxRecommended => Some(format!("{RECOMMENDED_FILE_SIZE}")),
Config::SysConfigKeys => Some(get_config_keys_string()),
_ => self.sql.get_raw_config(key.as_ref()).await?,
@@ -610,12 +605,6 @@ impl Context {
&& !self.get_config_bool(Config::Bot).await?)
}
/// Returns whether sync messages should be uploaded to the mvbox.
pub(crate) async fn should_move_sync_msgs(&self) -> Result<bool> {
Ok(self.get_config_bool(Config::MvboxMove).await?
|| !self.get_config_bool(Config::IsChatmail).await?)
}
/// Returns whether MDNs should be requested.
pub(crate) async fn should_request_mdns(&self) -> Result<bool> {
match self.get_config_bool_opt(Config::MdnsEnabled).await? {
@@ -819,42 +808,43 @@ impl Context {
self,
"Creating a pseudo configured account which will not be able to send or receive messages. Only meant for tests!"
);
ConfiguredLoginParam::from_json(&format!(
r#"{{"addr":"{addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#
))?
.save_to_transports_table(self, &EnteredLoginParam::default())
.await?;
add_pseudo_transport(self, addr).await?;
self.sql
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(addr))
.await?;
} else {
self.sql
.transaction(|transaction| {
if transaction.query_row(
"SELECT COUNT(*) FROM transports WHERE addr=?",
(addr,),
|row| {
let res: i64 = row.get(0)?;
Ok(res)
},
)? == 0
{
bail!("Address does not belong to any transport.");
}
transaction.execute(
"UPDATE config SET value=? WHERE keyname='configured_addr'",
(addr,),
)?;
// Clean up SMTP and IMAP APPEND queue.
//
// The messages in the queue have a different
// From address so we cannot send them over
// the new SMTP transport.
transaction.execute("DELETE FROM smtp", ())?;
transaction.execute("DELETE FROM imap_send", ())?;
Ok(())
})
.await?;
send_sync_transports(self).await?;
self.sql.uncache_raw_config("configured_addr").await;
}
self.sql
.transaction(|transaction| {
if transaction.query_row(
"SELECT COUNT(*) FROM transports WHERE addr=?",
(addr,),
|row| {
let res: i64 = row.get(0)?;
Ok(res)
},
)? == 0
{
bail!("Address does not belong to any transport.");
}
transaction.execute(
"UPDATE config SET value=? WHERE keyname='configured_addr'",
(addr,),
)?;
// Clean up SMTP and IMAP APPEND queue.
//
// The messages in the queue have a different
// From address so we cannot send them over
// the new SMTP transport.
transaction.execute("DELETE FROM smtp", ())?;
transaction.execute("DELETE FROM imap_send", ())?;
Ok(())
})
.await?;
self.sql.uncache_raw_config("configured_addr").await;
}
_ => {
self.sql.set_raw_config(key.as_ref(), value).await?;
@@ -884,7 +874,7 @@ impl Context {
{
return Ok(());
}
self.scheduler.interrupt_inbox().await;
self.scheduler.interrupt_smtp().await;
Ok(())
}
@@ -948,17 +938,7 @@ impl Context {
/// This should only be used by test code and during configure.
#[cfg(test)] // AEAP is disabled, but there are still tests for it
pub(crate) async fn set_primary_self_addr(&self, primary_new: &str) -> Result<()> {
self.quota.write().await.take();
// add old primary address (if exists) to secondary addresses
let mut secondary_addrs = self.get_all_self_addrs().await?;
// never store a primary address also as a secondary
secondary_addrs.retain(|a| !addr_cmp(a, primary_new));
self.set_config_internal(
Config::SecondaryAddrs,
Some(secondary_addrs.join(" ").as_str()),
)
.await?;
self.quota.write().await.clear();
self.sql
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(primary_new))
@@ -977,14 +957,10 @@ impl Context {
/// Returns all secondary self addresses.
pub(crate) async fn get_secondary_self_addrs(&self) -> Result<Vec<String>> {
let secondary_addrs = self
.get_config(Config::SecondaryAddrs)
.await?
.unwrap_or_default();
Ok(secondary_addrs
.split_ascii_whitespace()
.map(|s| s.to_string())
.collect())
self.sql.query_map_vec("SELECT addr FROM transports WHERE addr NOT IN (SELECT value FROM config WHERE keyname='configured_addr')", (), |row| {
let addr: String = row.get(0)?;
Ok(addr)
}).await
}
/// Returns the primary self address.

View File

@@ -94,59 +94,6 @@ async fn test_set_config_bool() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_self_addrs() -> Result<()> {
let alice = TestContext::new_alice().await;
assert!(alice.is_self_addr("alice@example.org").await?);
assert_eq!(alice.get_all_self_addrs().await?, vec!["alice@example.org"]);
assert!(!alice.is_self_addr("alice@alice.com").await?);
// Test adding the same primary address
alice.set_primary_self_addr("alice@example.org").await?;
alice.set_primary_self_addr("Alice@Example.Org").await?;
assert_eq!(alice.get_all_self_addrs().await?, vec!["Alice@Example.Org"]);
// Test adding a new (primary) self address
// The address is trimmed during configure by `LoginParam::from_database()`,
// so `set_primary_self_addr()` doesn't have to trim it.
alice.set_primary_self_addr("Alice@alice.com").await?;
assert!(alice.is_self_addr("aliCe@example.org").await?);
assert!(alice.is_self_addr("alice@alice.com").await?);
assert_eq!(
alice.get_all_self_addrs().await?,
vec!["Alice@alice.com", "Alice@Example.Org"]
);
// Check that the entry is not duplicated
alice.set_primary_self_addr("alice@alice.com").await?;
alice.set_primary_self_addr("alice@alice.com").await?;
assert_eq!(
alice.get_all_self_addrs().await?,
vec!["alice@alice.com", "Alice@Example.Org"]
);
// Test switching back
alice.set_primary_self_addr("alice@example.org").await?;
assert_eq!(
alice.get_all_self_addrs().await?,
vec!["alice@example.org", "alice@alice.com"]
);
// Test setting a new primary self address, the previous self address
// should be kept as a secondary self address
alice.set_primary_self_addr("alice@alice.xyz").await?;
assert_eq!(
alice.get_all_self_addrs().await?,
vec!["alice@alice.xyz", "alice@example.org", "alice@alice.com"]
);
assert!(alice.is_self_addr("alice@example.org").await?);
assert!(alice.is_self_addr("alice@alice.com").await?);
assert!(alice.is_self_addr("Alice@alice.xyz").await?);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mdns_default_behaviour() -> Result<()> {
let t = &TestContext::new_alice().await;
@@ -290,7 +237,7 @@ async fn test_no_sync_on_self_sent_msg() -> Result<()> {
let status = "Sent via usual message";
alice0.set_config(Config::Selfstatus, Some(status)).await?;
alice0.send_sync_msg().await?;
alice0.pop_sent_sync_msg().await;
alice0.pop_sent_msg().await;
let status1 = "Synced via sync message";
alice1.set_config(Config::Selfstatus, Some(status1)).await?;
tcm.send_recv(alice0, alice1, "hi Alice!").await;
@@ -314,7 +261,7 @@ async fn test_no_sync_on_self_sent_msg() -> Result<()> {
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
.await?;
alice0.send_sync_msg().await?;
alice0.pop_sent_sync_msg().await;
alice0.pop_sent_msg().await;
let file = alice1.dir.path().join("avatar.jpg");
let bytes = include_bytes!("../../test-data/image/avatar1000x1000.jpg");
tokio::fs::write(&file, bytes).await?;

View File

@@ -40,11 +40,15 @@ use crate::sync::Sync::*;
use crate::tools::time;
use crate::transport::{
ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
ConnectionCandidate,
ConnectionCandidate, send_sync_transports,
};
use crate::{EventType, stock_str};
use crate::{chat, provider};
/// Maximum number of relays
/// see <https://github.com/chatmail/core/issues/7608>
pub(crate) const MAX_TRANSPORT_RELAYS: usize = 5;
macro_rules! progress {
($context:tt, $progress:expr, $comment:expr) => {
assert!(
@@ -205,7 +209,9 @@ impl Context {
/// Removes the transport with the specified email address
/// (i.e. [EnteredLoginParam::addr]).
pub async fn delete_transport(&self, addr: &str) -> Result<()> {
self.sql
let now = time();
let removed_transport_id = self
.sql
.transaction(|transaction| {
let primary_addr = transaction.query_row(
"SELECT value FROM config WHERE keyname='configured_addr'",
@@ -219,12 +225,13 @@ impl Context {
if primary_addr == addr {
bail!("Cannot delete primary transport");
}
let transport_id = transaction.query_row(
"DELETE FROM transports WHERE addr=? RETURNING id",
let (transport_id, add_timestamp) = transaction.query_row(
"DELETE FROM transports WHERE addr=? RETURNING id, add_timestamp",
(addr,),
|row| {
let id: u32 = row.get(0)?;
Ok(id)
let add_timestamp: i64 = row.get(1)?;
Ok((id, add_timestamp))
},
)?;
transaction.execute("DELETE FROM imap WHERE transport_id=?", (transport_id,))?;
@@ -233,9 +240,24 @@ impl Context {
(transport_id,),
)?;
Ok(())
// Removal timestamp should not be lower than addition timestamp
// to be accepted by other devices when synced.
let remove_timestamp = std::cmp::max(now, add_timestamp);
transaction.execute(
"INSERT INTO removed_transports (addr, remove_timestamp)
VALUES (?, ?)
ON CONFLICT (addr)
DO UPDATE SET remove_timestamp = excluded.remove_timestamp",
(addr, remove_timestamp),
)?;
Ok(transport_id)
})
.await?;
send_sync_transports(self).await?;
self.quota.write().await.remove(&removed_transport_id);
Ok(())
}
@@ -252,18 +274,53 @@ impl Context {
)
.await?
{
if self.get_config(Config::MvboxMove).await?.as_deref() != Some("0") {
bail!("Cannot use multi-transport with mvbox_move enabled.");
}
// Should be checked before `MvboxMove` because the latter makes no sense in presense of
// `OnlyFetchMvbox` and even grayed out in the UIs in this case.
if self.get_config(Config::OnlyFetchMvbox).await?.as_deref() != Some("0") {
bail!("Cannot use multi-transport with only_fetch_mvbox enabled.");
bail!(
"To use additional relays, disable the legacy option \"Settings / Advanced / Only Fetch from DeltaChat Folder\"."
);
}
if self.get_config(Config::MvboxMove).await?.as_deref() != Some("0") {
bail!(
"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!("Cannot use multi-transport with disabled fetching of classic emails.");
bail!(
"To use additional relays, set the legacy option \"Settings / Advanced / Show Classic Emails\" to \"All\"."
);
}
if self
.sql
.count("SELECT COUNT(*) FROM transports", ())
.await?
>= MAX_TRANSPORT_RELAYS
{
bail!(
"You have reached the maximum number of relays ({}).",
MAX_TRANSPORT_RELAYS
)
}
}
let provider = configure(self, param).await?;
let provider = match configure(self, param).await {
Err(error) => {
// Log entered and actual params
let configured_param = get_configured_param(self, param).await;
warn!(
self,
"configure failed: Entered params: {}. Used params: {}. Error: {error}.",
param.to_string(),
configured_param
.map(|param| param.to_string())
.unwrap_or("error".to_owned())
);
return Err(error);
}
Ok(provider) => provider,
};
self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
.await?;
on_configure_completed(self, provider).await?;
@@ -552,7 +609,8 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
progress!(ctx, 900);
if !ctx.is_configured().await? {
let is_configured = ctx.is_configured().await?;
if !is_configured {
ctx.sql.set_raw_config("mvbox_move", Some("0")).await?;
ctx.sql.set_raw_config("only_fetch_mvbox", None).await?;
}
@@ -563,8 +621,10 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
let provider = configured_param.provider;
configured_param
.save_to_transports_table(ctx, param)
.clone()
.save_to_transports_table(ctx, param, time())
.await?;
send_sync_transports(ctx).await?;
ctx.set_config_internal(Config::ConfiguredTimestamp, Some(&time().to_string()))
.await?;

View File

@@ -2,16 +2,13 @@
#![allow(missing_docs)]
use std::sync::LazyLock;
use deltachat_derive::{FromSql, ToSql};
use percent_encoding::{AsciiSet, NON_ALPHANUMERIC};
use serde::{Deserialize, Serialize};
use crate::chat::ChatId;
pub static DC_VERSION_STR: LazyLock<String> =
LazyLock::new(|| env!("CARGO_PKG_VERSION").to_string());
pub static DC_VERSION_STR: &str = env!("CARGO_PKG_VERSION");
/// Set of characters to percent-encode in email addresses and names.
pub(crate) const NON_ALPHANUMERIC_WITHOUT_DOT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'.');

View File

@@ -36,7 +36,7 @@ use crate::message::MessageState;
use crate::mimeparser::AvatarAction;
use crate::param::{Param, Params};
use crate::sync::{self, Sync::*};
use crate::tools::{SystemTime, duration_to_str, get_abs_path, time, to_lowercase};
use crate::tools::{SystemTime, duration_to_str, get_abs_path, normalize_text, time, to_lowercase};
use crate::{chat, chatlist_events, ensure_and_debug_assert_ne, stock_str};
/// Time during which a contact is considered as seen recently.
@@ -115,9 +115,23 @@ impl ContactId {
let row = context
.sql
.transaction(|transaction| {
let authname;
let name_or_authname = if !name.is_empty() {
name
} else {
authname = transaction.query_row(
"SELECT authname FROM contacts WHERE id=?",
(self,),
|row| {
let authname: String = row.get(0)?;
Ok(authname)
},
)?;
&authname
};
let is_changed = transaction.execute(
"UPDATE contacts SET name=?1 WHERE id=?2 AND name!=?1",
(name, self),
"UPDATE contacts SET name=?1, name_normalized=?2 WHERE id=?3 AND name!=?1",
(name, normalize_text(name_or_authname), self),
)? > 0;
if is_changed {
update_chat_names(context, transaction, self)?;
@@ -967,11 +981,22 @@ impl Contact {
} else {
row_name
};
let new_authname = if update_authname {
name.to_string()
} else {
row_authname
};
transaction.execute(
"UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;",
"UPDATE contacts SET name=?, name_normalized=?, addr=?, origin=?, authname=? WHERE id=?",
(
new_name,
&new_name,
normalize_text(
if !new_name.is_empty() {
&new_name
} else {
&new_authname
}),
if update_addr {
addr.to_string()
} else {
@@ -982,11 +1007,7 @@ impl Contact {
} else {
row_origin
},
if update_authname {
name.to_string()
} else {
row_authname
},
&new_authname,
row_id,
),
)?;
@@ -998,18 +1019,18 @@ impl Contact {
sth_modified = Modifier::Modified;
}
} else {
let update_name = manual;
let update_authname = !manual;
transaction.execute(
"INSERT INTO contacts (name, addr, fingerprint, origin, authname)
VALUES (?, ?, ?, ?, ?);",
"
INSERT INTO contacts (name, name_normalized, addr, fingerprint, origin, authname)
VALUES (?, ?, ?, ?, ?, ?)
",
(
if update_name { &name } else { "" },
if manual { &name } else { "" },
normalize_text(&name),
&addr,
fingerprint,
origin,
if update_authname { &name } else { "" },
if manual { "" } else { &name },
),
)?;
@@ -1112,23 +1133,26 @@ impl Contact {
Origin::IncomingReplyTo
};
if query.is_some() {
let s3str_like_cmd = format!("%{}%", query.unwrap_or(""));
let s3str_like_cmd = format!("%{}%", query.unwrap_or("").to_lowercase());
context
.sql
.query_map(
"SELECT c.id, c.addr FROM contacts c
WHERE c.id>?
AND (c.fingerprint='')=?
AND c.origin>=? \
AND c.blocked=0 \
AND (iif(c.name='',c.authname,c.name) LIKE ? OR c.addr LIKE ?) \
ORDER BY c.last_seen DESC, c.id DESC;",
"
SELECT c.id, c.addr FROM contacts c
WHERE c.id>?
AND (c.fingerprint='')=?
AND c.origin>=?
AND c.blocked=0
AND (IFNULL(c.name_normalized,IIF(c.name='',c.authname,c.name)) LIKE ? OR c.addr LIKE ?)
ORDER BY c.origin>=? DESC, c.last_seen DESC, c.id DESC
",
(
ContactId::LAST_SPECIAL,
flag_address,
minimal_origin,
&s3str_like_cmd,
&s3str_like_cmd,
Origin::CreateChat,
),
|row| {
let id: ContactId = row.get(0)?;
@@ -1178,8 +1202,13 @@ impl Contact {
AND (fingerprint='')=?
AND origin>=?
AND blocked=0
ORDER BY last_seen DESC, id DESC;",
(ContactId::LAST_SPECIAL, flag_address, minimal_origin),
ORDER BY origin>=? DESC, last_seen DESC, id DESC",
(
ContactId::LAST_SPECIAL,
flag_address,
minimal_origin,
Origin::CreateChat,
),
|row| {
let id: ContactId = row.get(0)?;
let addr: String = row.get(1)?;
@@ -1249,8 +1278,18 @@ impl Contact {
};
// Always do an update in case the blocking is reset or name is changed.
transaction.execute(
"UPDATE contacts SET name=?, origin=?, blocked=1, fingerprint=? WHERE addr=?",
(&name, Origin::MailinglistAddress, fingerprint, &grpid),
"
UPDATE contacts
SET name=?, name_normalized=IIF(?1='',name_normalized,?), origin=?, blocked=1, fingerprint=?
WHERE addr=?
",
(
&name,
normalize_text(&name),
Origin::MailinglistAddress,
fingerprint,
&grpid,
),
)?;
}
Ok(())
@@ -1725,8 +1764,8 @@ fn update_chat_names(
};
let count = transaction.execute(
"UPDATE chats SET name=?1 WHERE id=?2 AND name!=?1",
(chat_name, chat_id),
"UPDATE chats SET name=?1, name_normalized=?2 WHERE id=?3 AND name!=?1",
(&chat_name, normalize_text(&chat_name), chat_id),
)?;
if count > 0 {

View File

@@ -60,16 +60,16 @@ async fn test_get_contacts() -> Result<()> {
let context = tcm.bob().await;
let alice = tcm.alice().await;
alice
.set_config(Config::Displayname, Some("MyName"))
.set_config(Config::Displayname, Some("MyNameIsΔ"))
.await?;
// Alice is not in the contacts yet.
let contacts = Contact::get_all(&context.ctx, 0, Some("Alice")).await?;
assert_eq!(contacts.len(), 0);
let contacts = Contact::get_all(&context.ctx, 0, Some("MyName")).await?;
let contacts = Contact::get_all(&context.ctx, 0, Some("MyNameIsΔ")).await?;
assert_eq!(contacts.len(), 0);
let claire_id = Contact::create(&context, "someone", "claire@example.org").await?;
let claire_id = Contact::create(&context, "Δ-someone", "claire@example.org").await?;
let dave_id = Contact::create(&context, "", "dave@example.org").await?;
let id = context.add_or_lookup_contact_id(&alice).await;
@@ -77,8 +77,8 @@ async fn test_get_contacts() -> Result<()> {
let contact = Contact::get_by_id(&context, id).await.unwrap();
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_authname(), "MyName");
assert_eq!(contact.get_display_name(), "MyName");
assert_eq!(contact.get_authname(), "MyNameIsΔ");
assert_eq!(contact.get_display_name(), "MyNameIsΔ");
// Search by name.
let contacts = Contact::get_all(&context, 0, Some("myname")).await?;
@@ -93,12 +93,12 @@ async fn test_get_contacts() -> Result<()> {
let contacts = Contact::get_all(&context, 0, Some("Foobar")).await?;
assert_eq!(contacts.len(), 0);
// Set Alice name to "someone" manually.
id.set_name(&context, "someone").await?;
// Set Alice name manually.
id.set_name(&context, "Δ-someone").await?;
let contact = Contact::get_by_id(&context.ctx, id).await.unwrap();
assert_eq!(contact.get_name(), "someone");
assert_eq!(contact.get_authname(), "MyName");
assert_eq!(contact.get_display_name(), "someone");
assert_eq!(contact.get_name(), "Δ-someone");
assert_eq!(contact.get_authname(), "MyNameIsΔ");
assert_eq!(contact.get_display_name(), "Δ-someone");
// Not searchable by authname, because it is not displayed.
let contacts = Contact::get_all(&context, 0, Some("MyName")).await?;
@@ -108,7 +108,9 @@ async fn test_get_contacts() -> Result<()> {
info!(&context, "add_self={add_self}");
// Search key-contacts by display name (same as manually set name).
let contacts = Contact::get_all(&context.ctx, add_self, Some("someone")).await?;
let contacts = Contact::get_all(&context.ctx, add_self, Some("Δ-someone")).await?;
assert_eq!(contacts, vec![id]);
let contacts = Contact::get_all(&context.ctx, add_self, Some("δ-someon")).await?;
assert_eq!(contacts, vec![id]);
// Get all key-contacts.
@@ -120,7 +122,7 @@ async fn test_get_contacts() -> Result<()> {
}
// Search address-contacts by display name.
let contacts = Contact::get_all(&context, constants::DC_GCL_ADDRESS, Some("someone")).await?;
let contacts = Contact::get_all(&context, constants::DC_GCL_ADDRESS, Some("Δ-someone")).await?;
assert_eq!(contacts, vec![claire_id]);
// Get all address-contacts. Newer contacts go first.
@@ -134,6 +136,16 @@ async fn test_get_contacts() -> Result<()> {
.await?;
assert_eq!(contacts, vec![dave_id, claire_id, ContactId::SELF]);
// Reset the user-provided name for Alice.
id.set_name(&context, "").await?;
let contact = Contact::get_by_id(&context.ctx, id).await.unwrap();
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_authname(), "MyNameIsΔ");
assert_eq!(contact.get_display_name(), "MyNameIsΔ");
let contacts = Contact::get_all(&context, 0, Some("MyName")).await?;
assert_eq!(contacts.len(), 1);
let contacts = Contact::get_all(&context, 0, Some("δ")).await?;
assert_eq!(contacts.len(), 1);
Ok(())
}

View File

@@ -5,7 +5,7 @@ use std::ffi::OsString;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, OnceLock};
use std::sync::{Arc, OnceLock, Weak};
use std::time::Duration;
use anyhow::{Context as _, Result, bail, ensure};
@@ -23,7 +23,6 @@ use crate::imap::{FolderMeaning, Imap, ServerMetadata};
use crate::key::self_fingerprint;
use crate::log::warn;
use crate::logged_debug_assert;
use crate::login_param::EnteredLoginParam;
use crate::message::{self, MessageState, MsgId};
use crate::net::tls::TlsSessionStore;
use crate::peer_channels::Iroh;
@@ -201,6 +200,25 @@ impl Deref for Context {
}
}
/// A weak reference to a [`Context`]
///
/// Can be used to obtain a [`Context`]. An existing weak reference does not prevent the corresponding [`Context`] from being dropped.
#[derive(Clone, Debug)]
pub(crate) struct WeakContext {
inner: Weak<InnerContext>,
}
impl WeakContext {
/// Returns the [`Context`] if it is still available.
pub(crate) fn upgrade(&self) -> Result<Context> {
let inner = self
.inner
.upgrade()
.ok_or_else(|| anyhow::anyhow!("Inner struct has been dropped"))?;
Ok(Context { inner })
}
}
/// Actual context, expensive to clone.
#[derive(Debug)]
pub struct InnerContext {
@@ -225,9 +243,9 @@ pub struct InnerContext {
pub(crate) scheduler: SchedulerState,
pub(crate) ratelimit: RwLock<Ratelimit>,
/// Recently loaded quota information, if any.
/// Set to `None` if quota was never tried to load.
pub(crate) quota: RwLock<Option<QuotaInfo>>,
/// Recently loaded quota information for each trasnport, if any.
/// If quota was never tried to load, then the transport doesn't have an entry in the BTreeMap.
pub(crate) quota: RwLock<BTreeMap<u32, QuotaInfo>>,
/// Notify about new messages.
///
@@ -333,7 +351,7 @@ pub fn get_info() -> BTreeMap<&'static str, String> {
#[cfg(not(debug_assertions))]
res.insert("debug_assertions", "Off".to_string());
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
res.insert("deltachat_core_version", format!("v{DC_VERSION_STR}"));
res.insert("sqlite_version", rusqlite::version().to_string());
res.insert("arch", (std::mem::size_of::<usize>() * 8).to_string());
res.insert("num_cpus", num_cpus::get().to_string());
@@ -385,6 +403,13 @@ impl Context {
Ok(context)
}
/// Returns a weak reference to this [`Context`].
pub(crate) fn get_weak_context(&self) -> WeakContext {
WeakContext {
inner: Arc::downgrade(&self.inner),
}
}
/// Opens the database with the given passphrase.
/// NB: Db encryption is deprecated, so `passphrase` should be empty normally. See
/// [`ContextBuilder::with_password()`] for reasoning.
@@ -454,7 +479,7 @@ impl Context {
events,
scheduler: SchedulerState::new(),
ratelimit: RwLock::new(Ratelimit::new(Duration::new(3, 0), 3.0)), // Allow at least 1 message every second + a burst of 3.
quota: RwLock::new(None),
quota: RwLock::new(BTreeMap::new()),
new_msgs_notify,
server_id: RwLock::new(None),
metadata: RwLock::new(None),
@@ -589,8 +614,13 @@ impl Context {
}
// Update quota (to send warning if full) - but only check it once in a while.
// note: For now this only checks quota of primary transport,
// because background check only checks primary transport at the moment
if self
.quota_needs_update(DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT)
.quota_needs_update(
session.transport_id(),
DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT,
)
.await
&& let Err(err) = self.update_recent_quota(&mut session).await
{
@@ -790,12 +820,17 @@ impl Context {
/// Returns information about the context as key-value pairs.
pub async fn get_info(&self) -> Result<BTreeMap<&'static str, String>> {
let l = EnteredLoginParam::load(self).await?;
let l2 = ConfiguredLoginParam::load(self).await?.map_or_else(
|| "Not configured".to_string(),
|(_transport_id, param)| param.to_string(),
);
let secondary_addrs = self.get_secondary_self_addrs().await?.join(", ");
let all_transports: Vec<String> = ConfiguredLoginParam::load_all(self)
.await?
.into_iter()
.map(|(transport_id, param)| format!("{transport_id}: {param}"))
.collect();
let all_transports = if all_transports.is_empty() {
"Not configured".to_string()
} else {
all_transports.join(",")
};
let chats = get_chat_cnt(self).await?;
let unblocked_msgs = message::get_unblocked_msg_cnt(self).await;
let request_msgs = message::get_request_msg_cnt(self).await;
@@ -874,8 +909,7 @@ impl Context {
.unwrap_or_else(|| "<unset>".to_string()),
);
res.insert("proxy_enabled", proxy_enabled.to_string());
res.insert("entered_account_settings", l.to_string());
res.insert("used_account_settings", l2);
res.insert("used_transport_settings", all_transports);
if let Some(server_id) = &*self.server_id.read().await {
res.insert("imap_server_id", format!("{server_id:?}"));
@@ -1305,10 +1339,5 @@ impl Context {
}
}
/// Returns core version as a string.
pub fn get_version_str() -> &'static str {
&DC_VERSION_STR
}
#[cfg(test)]
mod context_tests;

View File

@@ -153,11 +153,15 @@ pub(crate) async fn download_msg(
return Ok(());
};
let transport_id = session.transport_id();
let row = context
.sql
.query_row_optional(
"SELECT uid, folder FROM imap WHERE rfc724_mid=? AND target!=''",
(&msg.rfc724_mid,),
"SELECT uid, folder FROM imap
WHERE rfc724_mid=?
AND transport_id=?
AND target!=''",
(&msg.rfc724_mid, transport_id),
|row| {
let server_uid: u32 = row.get(0)?;
let server_folder: String = row.get(1)?;

View File

@@ -8,33 +8,27 @@ use mail_builder::mime::MimePart;
use crate::aheader::{Aheader, EncryptPreference};
use crate::context::Context;
use crate::key::{SignedPublicKey, load_self_public_key, load_self_secret_key};
use crate::pgp;
use crate::pgp::{self, SeipdVersion};
#[derive(Debug)]
pub struct EncryptHelper {
pub prefer_encrypt: EncryptPreference,
pub addr: String,
pub public_key: SignedPublicKey,
}
impl EncryptHelper {
pub async fn new(context: &Context) -> Result<EncryptHelper> {
let prefer_encrypt = EncryptPreference::Mutual;
let addr = context.get_primary_self_addr().await?;
let public_key = load_self_public_key(context).await?;
Ok(EncryptHelper {
prefer_encrypt,
addr,
public_key,
})
Ok(EncryptHelper { addr, public_key })
}
pub fn get_aheader(&self) -> Aheader {
Aheader {
addr: self.addr.clone(),
public_key: self.public_key.clone(),
prefer_encrypt: self.prefer_encrypt,
prefer_encrypt: EncryptPreference::Mutual,
verified: false,
}
}
@@ -47,6 +41,7 @@ impl EncryptHelper {
mail_to_encrypt: MimePart<'static>,
compress: bool,
anonymous_recipients: bool,
seipd_version: SeipdVersion,
) -> Result<String> {
let sign_key = load_self_secret_key(context).await?;
@@ -60,6 +55,7 @@ impl EncryptHelper {
sign_key,
compress,
anonymous_recipients,
seipd_version,
)
.await?;

View File

@@ -30,9 +30,10 @@ pub(crate) async fn emit_chatlist_item_changed_for_contact_chat(
match ChatId::lookup_by_contact(context, contact_id).await {
Ok(Some(chat_id)) => self::emit_chatlist_item_changed(context, chat_id),
Ok(None) => {}
Err(error) => context.emit_event(EventType::Error(format!(
Err(error) => error!(
context,
"failed to find chat id for contact for chatlist event: {error:?}"
))),
),
}
}

View File

@@ -243,7 +243,7 @@ pub enum EventType {
/// Progress.
///
/// 0=error, 1-999=progress in permille, 1000=success and done
progress: usize,
progress: u16,
/// Progress comment or error, something to display to the user.
comment: Option<String>,
@@ -253,7 +253,7 @@ pub enum EventType {
///
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
/// @param data2 0
ImexProgress(usize),
ImexProgress(u16),
/// A file has been exported. A file has been written by imex().
/// This event may be sent multiple times by a single call to imex().
@@ -280,7 +280,7 @@ pub enum EventType {
chat_type: Chattype,
/// Progress, always 1000.
progress: usize,
progress: u16,
},
/// Progress information of a secure-join handshake from the view of the joiner
@@ -295,7 +295,7 @@ pub enum EventType {
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
/// (Bob has verified alice and waits until Alice does the same for him)
/// 1000=vg-member-added/vc-contact-confirm received
progress: usize,
progress: u16,
},
/// The connectivity to the server changed.
@@ -417,6 +417,15 @@ pub enum EventType {
chat_id: ChatId,
},
/// One or more transports has changed.
///
/// UI should update the list.
///
/// This event is emitted when transport
/// synchronization messages arrives,
/// but not when the UI modifies the transport list by itself.
TransportsModified,
/// Event for using in tests, e.g. as a fence between normally generated events.
#[cfg(test)]
Test,

View File

@@ -27,7 +27,7 @@ use crate::calls::{create_fallback_ice_servers, create_ice_servers_from_metadata
use crate::chat::{self, ChatId, ChatIdBlocked, add_device_msg};
use crate::chatlist_events;
use crate::config::Config;
use crate::constants::{self, Blocked, Chattype, ShowEmails};
use crate::constants::{self, Blocked, Chattype, DC_VERSION_STR, ShowEmails};
use crate::contact::{Contact, ContactId, Modifier, Origin};
use crate::context::Context;
use crate::events::EventType;
@@ -123,7 +123,7 @@ struct OAuth2 {
access_token: String,
}
#[derive(Debug)]
#[derive(Debug, Default)]
pub(crate) struct ServerMetadata {
/// IMAP METADATA `/shared/comment` as defined in
/// <https://www.rfc-editor.org/rfc/rfc5464#section-6.2.1>.
@@ -916,7 +916,7 @@ impl Session {
context
.sql
.transaction(move |transaction| {
transaction.execute("DELETE FROM imap WHERE folder=?", (folder,))?;
transaction.execute("DELETE FROM imap WHERE transport_id=? AND folder=?", (transport_id, folder,))?;
for (uid, (rfc724_mid, target)) in &msgs {
// This may detect previously undetected moved
// messages, so we update server_folder too.
@@ -1054,14 +1054,16 @@ impl Session {
///
/// This is the only place where messages are moved or deleted on the IMAP server.
async fn move_delete_messages(&mut self, context: &Context, folder: &str) -> Result<()> {
let transport_id = self.transport_id();
let rows = context
.sql
.query_map_vec(
"SELECT id, uid, target FROM imap
WHERE folder = ?
AND target != folder
ORDER BY target, uid",
(folder,),
WHERE folder = ?
AND transport_id = ?
AND target != folder
ORDER BY target, uid",
(folder, transport_id),
|row| {
let rowid: i64 = row.get(0)?;
let uid: u32 = row.get(1)?;
@@ -1108,52 +1110,6 @@ impl Session {
Ok(())
}
/// Uploads sync messages from the `imap_send` table with `\Seen` flag set.
pub(crate) async fn send_sync_msgs(&mut self, context: &Context, folder: &str) -> Result<()> {
context.send_sync_msg().await?;
while let Some((id, mime, msg_id, attempts)) = context
.sql
.query_row_optional(
"SELECT id, mime, msg_id, attempts FROM imap_send ORDER BY id LIMIT 1",
(),
|row| {
let id: i64 = row.get(0)?;
let mime: String = row.get(1)?;
let msg_id: MsgId = row.get(2)?;
let attempts: i64 = row.get(3)?;
Ok((id, mime, msg_id, attempts))
},
)
.await
.context("Failed to SELECT from imap_send")?
{
let res = self
.append(folder, Some("(\\Seen)"), None, mime)
.await
.with_context(|| format!("IMAP APPEND to {folder} failed for {msg_id}"))
.log_err(context);
if res.is_ok() {
msg_id.set_delivered(context).await?;
}
const MAX_ATTEMPTS: i64 = 2;
if res.is_ok() || attempts >= MAX_ATTEMPTS - 1 {
context
.sql
.execute("DELETE FROM imap_send WHERE id=?", (id,))
.await
.context("Failed to delete from imap_send")?;
} else {
context
.sql
.execute("UPDATE imap_send SET attempts=attempts+1 WHERE id=?", (id,))
.await
.context("Failed to update imap_send.attempts")?;
res?;
}
}
Ok(())
}
/// Stores pending `\Seen` flags for messages in `imap_markseen` table.
pub(crate) async fn store_seen_flags_on_imap(&mut self, context: &Context) -> Result<()> {
let rows = context
@@ -1277,10 +1233,10 @@ impl Session {
};
let is_seen = fetch.flags().any(|flag| flag == Flag::Seen);
if is_seen
&& let Some(chat_id) = mark_seen_by_uid(context, folder, uid_validity, uid)
&& let Some(chat_id) = mark_seen_by_uid(context, transport_id, folder, uid_validity, uid)
.await
.with_context(|| {
format!("failed to update seen status for msg {folder}/{uid}")
format!("Transport {transport_id}: Failed to update seen status for msg {folder}/{uid}")
})?
{
updated_chat_ids.insert(chat_id);
@@ -1500,7 +1456,7 @@ impl Session {
warn!(context, "receive_imf error: {err:#}.");
let text = format!(
"❌ Failed to receive a message: {err:#}. Please report this bug to delta@merlinux.eu or https://support.delta.chat/."
"❌ Failed to receive a message: {err:#}. Core version v{DC_VERSION_STR}. Please report this bug to delta@merlinux.eu or https://support.delta.chat/.",
);
let mut msg = Message::new_text(text);
add_device_msg(context, None, Some(&mut msg)).await?;
@@ -1546,17 +1502,17 @@ impl Session {
Ok(())
}
/// Retrieves server metadata if it is supported.
/// Retrieves server metadata if it is supported, otherwise uses fallback one.
///
/// We get [`/shared/comment`](https://www.rfc-editor.org/rfc/rfc5464#section-6.2.1)
/// and [`/shared/admin`](https://www.rfc-editor.org/rfc/rfc5464#section-6.2.2)
/// metadata.
pub(crate) async fn fetch_metadata(&mut self, context: &Context) -> Result<()> {
if !self.can_metadata() {
return Ok(());
}
pub(crate) async fn update_metadata(&mut self, context: &Context) -> Result<()> {
let mut lock = context.metadata.write().await;
if !self.can_metadata() {
*lock = Some(Default::default());
}
if let Some(ref mut old_metadata) = *lock {
let now = time();
@@ -1565,31 +1521,33 @@ impl Session {
return Ok(());
}
info!(context, "ICE servers expired, requesting new credentials.");
let mailbox = "";
let options = "";
let metadata = self
.get_metadata(mailbox, options, "(/shared/vendor/deltachat/turn)")
.await?;
let mut got_turn_server = false;
for m in metadata {
if m.entry == "/shared/vendor/deltachat/turn"
&& let Some(value) = m.value
{
match create_ice_servers_from_metadata(context, &value).await {
Ok((parsed_timestamp, parsed_ice_servers)) => {
old_metadata.ice_servers_expiration_timestamp = parsed_timestamp;
old_metadata.ice_servers = parsed_ice_servers;
got_turn_server = false;
}
Err(err) => {
warn!(context, "Failed to parse TURN server metadata: {err:#}.");
if self.can_metadata() {
info!(context, "ICE servers expired, requesting new credentials.");
let mailbox = "";
let options = "";
let metadata = self
.get_metadata(mailbox, options, "(/shared/vendor/deltachat/turn)")
.await?;
for m in metadata {
if m.entry == "/shared/vendor/deltachat/turn"
&& let Some(value) = m.value
{
match create_ice_servers_from_metadata(context, &value).await {
Ok((parsed_timestamp, parsed_ice_servers)) => {
old_metadata.ice_servers_expiration_timestamp = parsed_timestamp;
old_metadata.ice_servers = parsed_ice_servers;
got_turn_server = true;
}
Err(err) => {
warn!(context, "Failed to parse TURN server metadata: {err:#}.");
}
}
}
}
}
if !got_turn_server {
info!(context, "Will use fallback ICE servers.");
// Set expiration timestamp 7 days in the future so we don't request it again.
old_metadata.ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
old_metadata.ice_servers = create_fallback_ice_servers(context).await?;
@@ -2109,17 +2067,6 @@ async fn needs_move_to_mvbox(
headers: &[mailparse::MailHeader<'_>],
) -> Result<bool> {
let has_chat_version = headers.get_header_value(HeaderDef::ChatVersion).is_some();
if !context.get_config_bool(Config::IsChatmail).await?
&& has_chat_version
&& headers
.get_header_value(HeaderDef::AutoSubmitted)
.filter(|val| val.eq_ignore_ascii_case("auto-generated"))
.is_some()
&& let Some(from) = mimeparser::get_from(headers)
&& context.is_self_addr(&from.addr).await?
{
return Ok(true);
}
if !context.get_config_bool(Config::MvboxMove).await? {
return Ok(false);
}
@@ -2357,6 +2304,7 @@ pub(crate) async fn prefetch_should_download(
/// Returns updated chat ID if any message was marked as seen.
async fn mark_seen_by_uid(
context: &Context,
transport_id: u32,
folder: &str,
uid_validity: u32,
uid: u32,
@@ -2367,12 +2315,13 @@ async fn mark_seen_by_uid(
"SELECT id, chat_id FROM msgs
WHERE id > 9 AND rfc724_mid IN (
SELECT rfc724_mid FROM imap
WHERE folder=?1
AND uidvalidity=?2
AND uid=?3
WHERE transport_id=?
AND folder=?
AND uidvalidity=?
AND uid=?
LIMIT 1
)",
(&folder, uid_validity, uid),
(transport_id, &folder, uid_validity, uid),
|row| {
let msg_id: MsgId = row.get(0)?;
let chat_id: ChatId = row.get(1)?;

View File

@@ -1,5 +1,6 @@
use super::*;
use crate::test_utils::TestContext;
use crate::transport::add_pseudo_transport;
#[test]
fn test_get_folder_meaning_by_name() {
@@ -271,12 +272,14 @@ async fn test_get_imap_search_command() -> Result<()> {
r#"FROM "alice@example.org""#
);
add_pseudo_transport(&t, "alice@another.com").await?;
t.ctx.set_primary_self_addr("alice@another.com").await?;
assert_eq!(
get_imap_self_sent_search_command(&t.ctx).await?,
r#"OR (FROM "alice@another.com") (FROM "alice@example.org")"#
);
add_pseudo_transport(&t, "alice@third.com").await?;
t.ctx.set_primary_self_addr("alice@third.com").await?;
assert_eq!(
get_imap_self_sent_search_command(&t.ctx).await?,

View File

@@ -25,7 +25,8 @@ use crate::pgp;
use crate::qr::DCBACKUP_VERSION;
use crate::sql;
use crate::tools::{
TempPathGuard, create_folder, delete_file, get_filesuffix_lc, read_file, time, write_file,
TempPathGuard, create_folder, delete_file, get_filesuffix_lc, read_file, time, usize_to_u64,
write_file,
};
mod key_transfer;
@@ -263,14 +264,14 @@ struct ProgressReader<R> {
inner: R,
/// Number of bytes successfully read from the internal reader.
read: usize,
read: u64,
/// Total size of the backup .tar file expected to be read from the reader.
/// Used to calculate the progress.
file_size: usize,
file_size: u64,
/// Last progress emitted to avoid emitting the same progress value twice.
last_progress: usize,
last_progress: u16,
/// Context for emitting progress events.
context: Context,
@@ -281,7 +282,7 @@ impl<R> ProgressReader<R> {
Self {
inner: r,
read: 0,
file_size: file_size as usize,
file_size,
last_progress: 1,
context,
}
@@ -301,9 +302,11 @@ where
let before = buf.filled().len();
let res = this.inner.poll_read(cx, buf);
if let std::task::Poll::Ready(Ok(())) = res {
*this.read = this.read.saturating_add(buf.filled().len() - before);
*this.read = this
.read
.saturating_add(usize_to_u64(buf.filled().len() - before));
let progress = std::cmp::min(1000 * *this.read / *this.file_size, 999);
let progress = std::cmp::min(1000 * *this.read / *this.file_size, 999) as u16;
if progress > *this.last_progress {
this.context.emit_event(EventType::ImexProgress(progress));
*this.last_progress = progress;
@@ -490,14 +493,14 @@ struct ProgressWriter<W> {
inner: W,
/// Number of bytes successfully written into the internal writer.
written: usize,
written: u64,
/// Total size of the backup .tar file expected to be written into the writer.
/// Used to calculate the progress.
file_size: usize,
file_size: u64,
/// Last progress emitted to avoid emitting the same progress value twice.
last_progress: usize,
last_progress: u16,
/// Context for emitting progress events.
context: Context,
@@ -508,7 +511,7 @@ impl<W> ProgressWriter<W> {
Self {
inner: w,
written: 0,
file_size: file_size as usize,
file_size,
last_progress: 1,
context,
}
@@ -527,9 +530,9 @@ where
let this = self.project();
let res = this.inner.poll_write(cx, buf);
if let std::task::Poll::Ready(Ok(written)) = res {
*this.written = this.written.saturating_add(written);
*this.written = this.written.saturating_add(usize_to_u64(written));
let progress = std::cmp::min(1000 * *this.written / *this.file_size, 999);
let progress = std::cmp::min(1000 * *this.written / *this.file_size, 999) as u16;
if progress > *this.last_progress {
this.context.emit_event(EventType::ImexProgress(progress));
*this.last_progress = progress;

View File

@@ -15,16 +15,17 @@ use pin_project::pin_project;
use crate::events::{Event, EventType, Events};
use crate::net::session::SessionStream;
use crate::tools::usize_to_u64;
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
#[derive(Debug)]
struct Metrics {
/// Total number of bytes read.
pub total_read: usize,
pub total_read: u64,
/// Total number of bytes written.
pub total_written: usize,
pub total_written: u64,
}
impl Metrics {
@@ -91,6 +92,11 @@ impl<S: SessionStream> AsyncRead for LoggingStream<S> {
"Read error on stream {peer_addr:?} after reading {} and writing {} bytes: {err}.",
this.metrics.total_read, this.metrics.total_written
);
tracing::event!(
::tracing::Level::WARN,
account_id = *this.account_id,
log_message
);
this.events.emit(Event {
id: *this.account_id,
typ: EventType::Warning(log_message),
@@ -98,7 +104,7 @@ impl<S: SessionStream> AsyncRead for LoggingStream<S> {
}
let n = old_remaining - buf.remaining();
this.metrics.total_read = this.metrics.total_read.saturating_add(n);
this.metrics.total_read = this.metrics.total_read.saturating_add(usize_to_u64(n));
res
}
@@ -113,7 +119,7 @@ impl<S: SessionStream> AsyncWrite for LoggingStream<S> {
let this = self.project();
let res = this.inner.poll_write(cx, buf);
if let Poll::Ready(Ok(n)) = res {
this.metrics.total_written = this.metrics.total_written.saturating_add(n);
this.metrics.total_written = this.metrics.total_written.saturating_add(usize_to_u64(n));
}
res
}
@@ -140,7 +146,7 @@ impl<S: SessionStream> AsyncWrite for LoggingStream<S> {
let this = self.project();
let res = this.inner.poll_write_vectored(cx, bufs);
if let Poll::Ready(Ok(n)) = res {
this.metrics.total_written = this.metrics.total_written.saturating_add(n);
this.metrics.total_written = this.metrics.total_written.saturating_add(usize_to_u64(n));
}
res
}

View File

@@ -171,12 +171,17 @@ impl MsgId {
context
.sql
.query_map_vec(
"SELECT folder, uid FROM imap WHERE rfc724_mid=?",
"SELECT transports.addr, imap.folder, imap.uid
FROM imap
LEFT JOIN transports
ON transports.id = imap.transport_id
WHERE imap.rfc724_mid=?",
(rfc724_mid,),
|row| {
let folder: String = row.get("folder")?;
let uid: u32 = row.get("uid")?;
Ok(format!("</{folder}/;UID={uid}>"))
let addr: String = row.get(0)?;
let folder: String = row.get(1)?;
let uid: u32 = row.get(2)?;
Ok(format!("<{addr}/{folder}/;UID={uid}>"))
},
)
.await
@@ -1712,6 +1717,7 @@ pub async fn delete_msgs_ex(
msgs: deleted_rfc724_mid,
})
.await?;
context.scheduler.interrupt_smtp().await;
}
for &msg_id in msg_ids {
@@ -2248,14 +2254,5 @@ impl Viewtype {
}
}
/// Returns text for storing in the `msgs.txt_normalized` column (to make case-insensitive search
/// possible for non-ASCII messages).
pub(crate) fn normalize_text(text: &str) -> Option<String> {
if text.is_ascii() {
return None;
};
Some(text.to_lowercase()).filter(|t| t != text)
}
#[cfg(test)]
mod message_tests;

View File

@@ -32,6 +32,7 @@ use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::{SystemMessage, is_hidden};
use crate::param::Param;
use crate::peer_channels::{create_iroh_header, get_iroh_topic_for_msg};
use crate::pgp::SeipdVersion;
use crate::simplify::escape_message_footer_marks;
use crate::stock_str;
use crate::tools::{
@@ -1258,6 +1259,17 @@ impl MimeFactory {
} else {
// Asymmetric encryption
let seipd_version = if encryption_pubkeys.is_empty() {
// If message is sent only to self,
// use v2 SEIPD.
SeipdVersion::V2
} else {
// If message is sent to others,
// they may not support v2 SEIPD yet,
// so use v1 SEIPD.
SeipdVersion::V1
};
// Encrypt to self unconditionally,
// even for a single-device setup.
let mut encryption_keyring = vec![encrypt_helper.public_key.clone()];
@@ -1271,6 +1283,7 @@ impl MimeFactory {
message,
compress,
anonymous_recipients,
seipd_version,
)
.await?
};

View File

@@ -40,7 +40,7 @@
//! used for successful connection timestamp of
//! retrieving them from in-memory cache is used.
use anyhow::{Context as _, Result};
use anyhow::{Context as _, Result, ensure};
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::str::FromStr;
@@ -506,10 +506,6 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
"mail.nubo.coop",
vec![IpAddr::V4(Ipv4Addr::new(79, 99, 201, 10))],
),
(
"mehl.cloud",
vec![IpAddr::V4(Ipv4Addr::new(95, 217, 223, 172))],
),
(
"mx.freenet.de",
vec![
@@ -680,6 +676,72 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
IpAddr::V4(Ipv4Addr::new(185, 230, 214, 164)),
],
),
// Known public chatmail relays from https://chatmail.at/relays
(
"mehl.cloud",
vec![IpAddr::V4(Ipv4Addr::new(95, 217, 223, 172))],
),
(
"mailchat.pl",
vec![IpAddr::V4(Ipv4Addr::new(46, 62, 144, 137))],
),
(
"chatmail.woodpeckersnest.space",
vec![IpAddr::V4(Ipv4Addr::new(85, 215, 162, 146))],
),
(
"chatmail.culturanerd.it",
vec![IpAddr::V4(Ipv4Addr::new(82, 165, 94, 165))],
),
(
"chatmail.hackea.org",
vec![IpAddr::V4(Ipv4Addr::new(82, 165, 11, 85))],
),
(
"chika.aangat.lahat.computer",
vec![IpAddr::V4(Ipv4Addr::new(71, 19, 150, 113))],
),
(
"tarpit.fun",
vec![IpAddr::V4(Ipv4Addr::new(152, 53, 86, 246))],
),
(
"d.gaufr.es",
vec![IpAddr::V4(Ipv4Addr::new(51, 77, 140, 91))],
),
(
"chtml.ca",
vec![IpAddr::V4(Ipv4Addr::new(51, 222, 156, 177))],
),
(
"chatmail.au",
vec![IpAddr::V4(Ipv4Addr::new(45, 124, 54, 79))],
),
(
"sombras.chat",
vec![IpAddr::V4(Ipv4Addr::new(82, 25, 70, 154))],
),
(
"e2ee.wang",
vec![IpAddr::V4(Ipv4Addr::new(139, 84, 233, 161))],
),
(
"chat.privittytech.com",
vec![IpAddr::V4(Ipv4Addr::new(35, 154, 144, 0))],
),
("e2ee.im", vec![IpAddr::V4(Ipv4Addr::new(45, 137, 99, 57))]),
(
"chatmail.email",
vec![IpAddr::V4(Ipv4Addr::new(57, 128, 220, 120))],
),
(
"danneskjold.de",
vec![IpAddr::V4(Ipv4Addr::new(46, 62, 216, 132))],
),
(
"darkrun.dev",
vec![IpAddr::V4(Ipv4Addr::new(72, 11, 149, 146))],
),
])
});
@@ -788,7 +850,7 @@ pub(crate) async fn lookup_host_with_cache(
}
};
if load_cache {
let addrs = if load_cache {
let mut cache = lookup_cache(context, hostname, port, alpn, now).await?;
if let Some(ips) = DNS_PRELOAD.get(hostname) {
for ip in ips {
@@ -799,10 +861,15 @@ pub(crate) async fn lookup_host_with_cache(
}
}
Ok(merge_with_cache(resolved_addrs, cache))
merge_with_cache(resolved_addrs, cache)
} else {
Ok(resolved_addrs)
}
resolved_addrs
};
ensure!(
!addrs.is_empty(),
"Could not find DNS resolutions for {hostname}:{port}. Check server hostname and your network"
);
Ok(addrs)
}
/// Merges results received from DNS with cached results.

View File

@@ -433,6 +433,14 @@ impl Params {
self.set(key, format!("{value}"));
self
}
pub fn steal(&mut self, src: &mut Self, key: Param) -> &mut Self {
let val = src.inner.remove(&key);
if let Some(val) = val {
self.inner.insert(key, val);
}
self
}
}
#[cfg(test)]

View File

@@ -160,6 +160,20 @@ fn select_pk_for_encryption(key: &SignedPublicKey) -> Option<&SignedPublicSubKey
.find(|subkey| subkey.is_encryption_key())
}
/// Version of SEIPD packet to use.
///
/// See
/// <https://www.rfc-editor.org/rfc/rfc9580#name-avoiding-ciphertext-malleab>
/// for the discussion on when v2 SEIPD should be used.
#[derive(Debug)]
pub enum SeipdVersion {
/// Use v1 SEIPD, for compatibility.
V1,
/// Use v2 SEIPD when we know that v2 SEIPD is supported.
V2,
}
/// Encrypts `plain` text using `public_keys_for_encryption`
/// and signs it using `private_key_for_signing`.
pub async fn pk_encrypt(
@@ -168,6 +182,7 @@ pub async fn pk_encrypt(
private_key_for_signing: SignedSecretKey,
compress: bool,
anonymous_recipients: bool,
seipd_version: SeipdVersion,
) -> Result<String> {
Handle::current()
.spawn_blocking(move || {
@@ -178,21 +193,49 @@ pub async fn pk_encrypt(
.filter_map(select_pk_for_encryption);
let msg = MessageBuilder::from_bytes("", plain);
let mut msg = msg.seipd_v1(&mut rng, SYMMETRIC_KEY_ALGORITHM);
for pkey in pkeys {
if anonymous_recipients {
msg.encrypt_to_key_anonymous(&mut rng, &pkey)?;
} else {
msg.encrypt_to_key(&mut rng, &pkey)?;
let encoded_msg = match seipd_version {
SeipdVersion::V1 => {
let mut msg = msg.seipd_v1(&mut rng, SYMMETRIC_KEY_ALGORITHM);
for pkey in pkeys {
if anonymous_recipients {
msg.encrypt_to_key_anonymous(&mut rng, &pkey)?;
} else {
msg.encrypt_to_key(&mut rng, &pkey)?;
}
}
msg.sign(&*private_key_for_signing, Password::empty(), HASH_ALGORITHM);
if compress {
msg.compression(CompressionAlgorithm::ZLIB);
}
msg.to_armored_string(&mut rng, Default::default())?
}
}
SeipdVersion::V2 => {
let mut msg = msg.seipd_v2(
&mut rng,
SYMMETRIC_KEY_ALGORITHM,
AeadAlgorithm::Ocb,
ChunkSize::C8KiB,
);
msg.sign(&*private_key_for_signing, Password::empty(), HASH_ALGORITHM);
if compress {
msg.compression(CompressionAlgorithm::ZLIB);
}
for pkey in pkeys {
if anonymous_recipients {
msg.encrypt_to_key_anonymous(&mut rng, &pkey)?;
} else {
msg.encrypt_to_key(&mut rng, &pkey)?;
}
}
let encoded_msg = msg.to_armored_string(&mut rng, Default::default())?;
msg.sign(&*private_key_for_signing, Password::empty(), HASH_ALGORITHM);
if compress {
msg.compression(CompressionAlgorithm::ZLIB);
}
msg.to_armored_string(&mut rng, Default::default())?
}
};
Ok(encoded_msg)
})
@@ -547,6 +590,7 @@ mod tests {
KEYS.alice_secret.clone(),
compress,
anonymous_recipients,
SeipdVersion::V2,
)
.await
.unwrap()
@@ -716,6 +760,7 @@ mod tests {
KEYS.alice_secret.clone(),
true,
true,
SeipdVersion::V2,
)
.await?;

View File

@@ -16,7 +16,6 @@ use serde::Deserialize;
use crate::config::Config;
use crate::contact::{Contact, ContactId, Origin};
use crate::context::Context;
use crate::events::EventType;
use crate::key::Fingerprint;
use crate::login_param::{EnteredCertificateChecks, EnteredLoginParam, EnteredServerLoginParam};
use crate::net::http::post_empty;
@@ -824,9 +823,10 @@ pub(crate) async fn login_param_from_account_qr(
match serde_json::from_str::<CreateAccountErrorResponse>(&response_text) {
Ok(error) => Err(anyhow!(error.reason)),
Err(parse_error) => {
context.emit_event(EventType::Error(format!(
error!(
context,
"Cannot create account, server response could not be parsed:\n{parse_error:#}\nraw response:\n{response_text}"
)));
);
bail!("Cannot create account, unexpected server response:\n{response_text:?}")
}
}
@@ -904,7 +904,7 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
.await?;
token::save(context, token::Namespace::Auth, None, &authcode, timestamp).await?;
context.sync_qr_code_tokens(None).await?;
context.scheduler.interrupt_inbox().await;
context.scheduler.interrupt_smtp().await;
}
Qr::ReviveVerifyGroup {
invitenumber,
@@ -936,7 +936,7 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
)
.await?;
context.sync_qr_code_tokens(Some(&grpid)).await?;
context.scheduler.interrupt_inbox().await;
context.scheduler.interrupt_smtp().await;
}
Qr::Login { address, options } => {
let mut param = login_param_from_login_qr(&address, options)?;

View File

@@ -107,10 +107,10 @@ pub fn needs_quota_warning(curr_percentage: u64, warned_at_percentage: u64) -> b
impl Context {
/// Returns whether the quota value needs an update. If so, `update_recent_quota()` should be
/// called.
pub(crate) async fn quota_needs_update(&self, ratelimit_secs: u64) -> bool {
pub(crate) async fn quota_needs_update(&self, transport_id: u32, ratelimit_secs: u64) -> bool {
let quota = self.quota.read().await;
quota
.as_ref()
.get(&transport_id)
.filter(|quota| time_elapsed(&quota.modified) < Duration::from_secs(ratelimit_secs))
.is_none()
}
@@ -155,10 +155,13 @@ impl Context {
}
}
*self.quota.write().await = Some(QuotaInfo {
recent: quota,
modified: tools::Time::now(),
});
self.quota.write().await.insert(
session.transport_id(),
QuotaInfo {
recent: quota,
modified: tools::Time::now(),
},
);
self.emit_event(EventType::ConnectivityChanged);
Ok(())
@@ -203,27 +206,42 @@ mod tests {
let mut tcm = TestContextManager::new();
let t = &tcm.unconfigured().await;
const TIMEOUT: u64 = 60;
assert!(t.quota_needs_update(TIMEOUT).await);
assert!(t.quota_needs_update(0, TIMEOUT).await);
*t.quota.write().await = Some(QuotaInfo {
recent: Ok(Default::default()),
modified: tools::Time::now() - Duration::from_secs(TIMEOUT + 1),
});
assert!(t.quota_needs_update(TIMEOUT).await);
*t.quota.write().await = {
let mut map = BTreeMap::new();
map.insert(
0,
QuotaInfo {
recent: Ok(Default::default()),
modified: tools::Time::now() - Duration::from_secs(TIMEOUT + 1),
},
);
map
};
assert!(t.quota_needs_update(0, TIMEOUT).await);
*t.quota.write().await = Some(QuotaInfo {
recent: Ok(Default::default()),
modified: tools::Time::now(),
});
assert!(!t.quota_needs_update(TIMEOUT).await);
*t.quota.write().await = {
let mut map = BTreeMap::new();
map.insert(
0,
QuotaInfo {
recent: Ok(Default::default()),
modified: tools::Time::now(),
},
);
map
};
assert!(!t.quota_needs_update(0, TIMEOUT).await);
t.evtracker.clear_events();
t.set_primary_self_addr("new@addr").await?;
assert!(t.quota.read().await.is_none());
assert!(t.quota.read().await.is_empty());
t.evtracker
.get_matching(|evt| matches!(evt, EventType::ConnectivityChanged))
.await;
assert!(t.quota_needs_update(TIMEOUT).await);
assert!(t.quota_needs_update(0, TIMEOUT).await);
Ok(())
}
}

View File

@@ -43,7 +43,9 @@ use crate::simplify;
use crate::stats::STATISTICS_BOT_EMAIL;
use crate::stock_str;
use crate::sync::Sync::*;
use crate::tools::{self, buf_compress, remove_subject_prefix, validate_broadcast_secret};
use crate::tools::{
self, buf_compress, normalize_text, remove_subject_prefix, validate_broadcast_secret,
};
use crate::{chatlist_events, ensure_and_debug_assert, ensure_and_debug_assert_eq, location};
/// This is the struct that is returned after receiving one email (aka MIME message).
@@ -675,12 +677,22 @@ pub(crate) async fn receive_imf_inner(
let res = if mime_parser.incoming {
handle_securejoin_handshake(context, &mut mime_parser, from_id)
.await
.context("error in Secure-Join message handling")?
.with_context(|| {
format!(
"Error in Secure-Join '{}' message handling",
mime_parser.get_header(HeaderDef::SecureJoin).unwrap_or("")
)
})?
} else if let Some(to_id) = to_ids.first().copied().flatten() {
// handshake may mark contacts as verified and must be processed before chats are created
observe_securejoin_on_other_device(context, &mime_parser, to_id)
.await
.context("error in Secure-Join watching")?
.with_context(|| {
format!(
"Error in Secure-Join '{}' watching",
mime_parser.get_header(HeaderDef::SecureJoin).unwrap_or("")
)
})?
} else {
securejoin::HandshakeMessage::Propagate
};
@@ -827,6 +839,41 @@ pub(crate) async fn receive_imf_inner(
if let Some(ref sync_items) = mime_parser.sync_items {
if from_id == ContactId::SELF {
if mime_parser.was_encrypted() {
// Receiving encrypted message from self updates primary transport.
let from_addr = &mime_parser.from.addr;
let transport_changed = context
.sql
.transaction(|transaction| {
let transport_exists = transaction.query_row(
"SELECT COUNT(*) FROM transports WHERE addr=?",
(from_addr,),
|row| {
let count: i64 = row.get(0)?;
Ok(count > 0)
},
)?;
let transport_changed = if transport_exists {
transaction.execute(
"UPDATE config SET value=? WHERE keyname='configured_addr'",
(from_addr,),
)? > 0
} else {
warn!(
context,
"Received sync message from unknown address {from_addr:?}."
);
false
};
Ok(transport_changed)
})
.await?;
if transport_changed {
info!(context, "Primary transport changed to {from_addr:?}.");
context.sql.uncache_raw_config("configured_addr").await;
}
context
.execute_sync_items(sync_items, mime_parser.timestamp_sent)
.await;
@@ -2059,7 +2106,7 @@ RETURNING id
if trash { MessageState::Undefined } else { state },
if trash { MessengerMessage::No } else { is_dc_message },
if trash || hidden { "" } else { msg },
if trash || hidden { None } else { message::normalize_text(msg) },
if trash || hidden { None } else { normalize_text(msg) },
if trash || hidden { "" } else { &subject },
if trash {
"".to_string()
@@ -2471,10 +2518,11 @@ async fn lookup_or_create_adhoc_group(
id INTEGER PRIMARY KEY
) STRICT",
(),
)?;
)
.context("CREATE TEMP TABLE temp.contacts")?;
let mut stmt = t.prepare("INSERT INTO temp.contacts(id) VALUES (?)")?;
for &id in &contact_ids {
stmt.execute((id,))?;
stmt.execute((id,)).context("INSERT INTO temp.contacts")?;
}
let val = t
.query_row(
@@ -2496,8 +2544,10 @@ async fn lookup_or_create_adhoc_group(
Ok((id, blocked))
},
)
.optional()?;
t.execute("DROP TABLE temp.contacts", ())?;
.optional()
.context("Select chat with matching name and members")?;
t.execute("DROP TABLE temp.contacts", ())
.context("DROP TABLE temp.contacts")?;
Ok(val)
};
let query_only = true;
@@ -3053,7 +3103,10 @@ async fn apply_chat_name_and_avatar_changes(
info!(context, "Updating grpname for chat {}.", chat.id);
context
.sql
.execute("UPDATE chats SET name=? WHERE id=?;", (grpname, chat.id))
.execute(
"UPDATE chats SET name=?, name_normalized=? WHERE id=?",
(grpname, normalize_text(grpname), chat.id),
)
.await?;
*send_event_chat_modified = true;
}
@@ -3342,7 +3395,10 @@ async fn apply_mailinglist_changes(
info!(context, "Updating listname for chat {chat_id}.");
context
.sql
.execute("UPDATE chats SET name=? WHERE id=?;", (new_name, chat_id))
.execute(
"UPDATE chats SET name=?, name_normalized=? WHERE id=?",
(&new_name, normalize_text(&new_name), chat_id),
)
.await?;
context.emit_event(EventType::ChatModified(chat_id));
}

View File

@@ -3852,6 +3852,38 @@ async fn test_sync_member_list_on_rejoin() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_group_contacts_goto_bottom() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
let bob_id = alice.add_or_lookup_contact_id(bob).await;
let fiona_id = alice.add_or_lookup_contact_id(fiona).await;
let alice_chat_id = create_group(alice, "Testing contact list").await?;
add_contact_to_chat(alice, alice_chat_id, bob_id).await?;
add_contact_to_chat(alice, alice_chat_id, fiona_id).await?;
send_text_msg(alice, alice_chat_id, "hello".to_string()).await?;
bob.recv_msg(&alice.pop_sent_msg().await).await;
let bob_chat_id = bob.get_last_msg().await.chat_id;
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 3);
assert_eq!(Contact::get_all(bob, 0, None).await?.len(), 0);
bob_chat_id.accept(bob).await?;
let contacts = Contact::get_all(bob, 0, None).await?;
assert_eq!(contacts.len(), 2);
let bob_fiona_id = bob.add_or_lookup_contact_id(fiona).await;
assert_eq!(contacts[1], bob_fiona_id);
ChatId::create_for_contact(bob, bob_fiona_id).await?;
let contacts = Contact::get_all(bob, 0, None).await?;
assert_eq!(contacts.len(), 2);
assert_eq!(contacts[0], bob_fiona_id);
Ok(())
}
/// Test for the bug when remote group membership changes from outdated messages overrode local
/// ones. Especially that was a problem when a message is sent offline so that it doesn't
/// incorporate recent group membership changes.

View File

@@ -325,6 +325,8 @@ impl Drop for IoPausedGuard {
#[derive(Debug)]
struct SchedBox {
/// Hostname of used chatmail/email relay
host: String,
meaning: FolderMeaning,
conn_state: ImapConnectionState,
@@ -479,7 +481,7 @@ async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session)
}
// Update quota no more than once a minute.
if ctx.quota_needs_update(60).await
if ctx.quota_needs_update(session.transport_id(), 60).await
&& let Err(err) = ctx.update_recent_quota(&mut session).await
{
warn!(ctx, "Failed to update quota: {:#}.", err);
@@ -536,9 +538,9 @@ async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session)
.await
.context("Failed to download messages")?;
session
.fetch_metadata(ctx)
.update_metadata(ctx)
.await
.context("Failed to fetch metadata")?;
.context("update_metadata")?;
session
.register_token(ctx)
.await
@@ -570,28 +572,6 @@ async fn fetch_idle(
};
if folder_config == Config::ConfiguredInboxFolder {
let mvbox;
let syncbox = match ctx.should_move_sync_msgs().await? {
false => &watch_folder,
true => {
mvbox = ctx.get_config(Config::ConfiguredMvboxFolder).await?;
mvbox.as_deref().unwrap_or(&watch_folder)
}
};
if ctx
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default()
== connection.addr
{
session
.send_sync_msgs(ctx, syncbox)
.await
.context("fetch_idle: send_sync_msgs")
.log_err(ctx)
.ok();
}
session
.store_seen_flags_on_imap(ctx)
.await
@@ -881,7 +861,14 @@ impl Scheduler {
let ctx = ctx.clone();
task::spawn(inbox_loop(ctx, inbox_start_send, inbox_handlers))
};
let host = configured_login_param
.addr
.split("@")
.last()
.context("address has no host")?
.to_owned();
let inbox = SchedBox {
host: host.clone(),
meaning: FolderMeaning::Inbox,
conn_state,
handle,
@@ -897,6 +884,7 @@ impl Scheduler {
let meaning = FolderMeaning::Mvbox;
let handle = task::spawn(simple_imap_loop(ctx, start_send, handlers, meaning));
oboxes.push(SchedBox {
host,
meaning,
conn_state,
handle,

View File

@@ -373,7 +373,13 @@ impl Context {
InnerSchedulerState::Started(ref sched) => (
sched
.boxes()
.map(|b| (b.meaning, b.conn_state.state.connectivity.clone()))
.map(|b| {
(
b.host.clone(),
b.meaning,
b.conn_state.state.connectivity.clone(),
)
})
.collect::<Vec<_>>(),
sched.smtp.state.connectivity.clone(),
),
@@ -396,7 +402,7 @@ impl Context {
let watched_folders = get_watched_folder_configs(self).await?;
let incoming_messages = stock_str::incoming_messages(self).await;
ret += &format!("<h3>{incoming_messages}</h3><ul>");
for (folder, state) in &folders_states {
for (host, folder, state) in &folders_states {
let mut folder_added = false;
if let Some(config) = folder.to_config().filter(|c| watched_folders.contains(c)) {
@@ -407,7 +413,11 @@ impl Context {
ret += "<li>";
ret += &*detailed.to_icon();
ret += " <b>";
ret += &*escaper::encode_minimal(&foldername);
if folder == &FolderMeaning::Inbox {
ret += &*escaper::encode_minimal(host);
} else {
ret += &*escaper::encode_minimal(&foldername);
}
ret += ":</b> ";
ret += &*escaper::encode_minimal(&detailed.to_string_imap(self).await);
ret += "</li>";
@@ -452,21 +462,41 @@ impl Context {
// [======67%===== ]
// =============================================================================================
let domain =
&deltachat_contact_tools::EmailAddress::new(&self.get_primary_self_addr().await?)?
.domain;
let storage_on_domain =
escaper::encode_minimal(&stock_str::storage_on_domain(self, domain).await);
ret += &format!("<h3>{storage_on_domain}</h3><ul>");
ret += "<h3>Message Buffers</h3>";
let transports = self
.sql
.query_map_vec("SELECT id, addr FROM transports", (), |row| {
let transport_id: u32 = row.get(0)?;
let addr: String = row.get(1)?;
Ok((transport_id, addr))
})
.await?;
let quota = self.quota.read().await;
if let Some(quota) = &*quota {
ret += "<ul>";
for (transport_id, transport_addr) in transports {
let domain = &deltachat_contact_tools::EmailAddress::new(&transport_addr)
.map_or(transport_addr, |email| email.domain);
let domain_escaped = escaper::encode_minimal(domain);
let Some(quota) = quota.get(&transport_id) else {
let not_connected = stock_str::not_connected(self).await;
ret += &format!("<li>{domain_escaped} &middot; {not_connected}</li>");
continue;
};
match &quota.recent {
Err(e) => {
let error_escaped = escaper::encode_minimal(&e.to_string());
ret += &format!("<li>{domain_escaped} &middot; {error_escaped}</li>");
}
Ok(quota) => {
if !quota.is_empty() {
if quota.is_empty() {
ret += &format!(
"<li>{domain_escaped} &middot; Warning: {domain_escaped} claims to support quota but gives no information</li>"
);
} else {
for (root_name, resources) in quota {
use async_imap::types::QuotaResourceName::*;
for resource in resources {
ret += "<li>";
ret += &format!("<li>{domain_escaped} &middot; ");
// root name is empty eg. for gmail and redundant eg. for riseup.
// therefore, use it only if there are really several roots.
@@ -529,21 +559,9 @@ impl Context {
ret += "</li>";
}
}
} else {
let domain_escaped = escaper::encode_minimal(domain);
ret += &format!(
"<li>Warning: {domain_escaped} claims to support quota but gives no information</li>"
);
}
}
Err(e) => {
let error_escaped = escaper::encode_minimal(&e.to_string());
ret += &format!("<li>{error_escaped}</li>");
}
}
} else {
let not_connected = stock_str::not_connected(self).await;
ret += &format!("<li>{not_connected}</li>");
}
ret += "</ul>";

View File

@@ -160,7 +160,7 @@ pub async fn get_securejoin_qr(context: &Context, chat: Option<ChatId>) -> Resul
context
.sync_qr_code_tokens(Some(chat.grpid.as_str()))
.await?;
context.scheduler.interrupt_inbox().await;
context.scheduler.interrupt_smtp().await;
}
let chat_name = chat.get_name();
@@ -192,7 +192,7 @@ pub async fn get_securejoin_qr(context: &Context, chat: Option<ChatId>) -> Resul
.replace("%20", "+");
if sync_token {
context.sync_qr_code_tokens(None).await?;
context.scheduler.interrupt_inbox().await;
context.scheduler.interrupt_smtp().await;
}
format!(
"https://i.delta.chat/#{fingerprint}&i={invitenumber}&s={auth}&a={self_addr_urlencoded}&n={self_name_urlencoded}",
@@ -567,7 +567,7 @@ pub(crate) async fn handle_securejoin_handshake(
"vc-contact-confirm" => {
context.emit_event(EventType::SecurejoinJoinerProgress {
contact_id,
progress: JoinerProgress::Succeeded.to_usize(),
progress: JoinerProgress::Succeeded.into_u16(),
});
Ok(HandshakeMessage::Ignore)
}
@@ -590,7 +590,7 @@ pub(crate) async fn handle_securejoin_handshake(
context.emit_event(EventType::SecurejoinJoinerProgress {
contact_id,
progress: JoinerProgress::Succeeded.to_usize(),
progress: JoinerProgress::Succeeded.into_u16(),
});
Ok(HandshakeMessage::Propagate)
}

View File

@@ -5,14 +5,16 @@ use anyhow::{Context as _, Result};
use super::HandshakeMessage;
use super::qrinvite::QrInvite;
use crate::chat::{self, ChatId, is_contact_in_chat};
use crate::chatlist_events;
use crate::constants::{Blocked, Chattype};
use crate::contact::Origin;
use crate::context::Context;
use crate::events::EventType;
use crate::key::self_fingerprint;
use crate::message::{Message, Viewtype};
use crate::log::LogExt;
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::param::Param;
use crate::param::{Param, Params};
use crate::securejoin::{ContactId, encrypted_and_signed, verify_sender_by_fingerprint};
use crate::stock_str;
use crate::sync::Sync::*;
@@ -43,31 +45,21 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
// A 1:1 chat is needed to send messages to Alice. When joining a group this chat is
// hidden, if a user starts sending messages in it it will be unhidden in
// receive_imf.
let hidden = match invite {
QrInvite::Contact { .. } => Blocked::Not,
QrInvite::Group { .. } => Blocked::Yes,
QrInvite::Broadcast { .. } => Blocked::Yes,
};
// The 1:1 chat with the inviter
let private_chat_id =
ChatId::create_for_contact_with_blocked(context, invite.contact_id(), hidden)
.await
.with_context(|| format!("can't create chat for contact {}", invite.contact_id()))?;
let private_chat_id = private_chat_id(context, &invite).await?;
ContactId::scaleup_origin(context, &[invite.contact_id()], Origin::SecurejoinJoined).await?;
context.emit_event(EventType::ContactsChanged(None));
let has_key = context
.sql
.exists(
"SELECT COUNT(*) FROM public_keys WHERE fingerprint=?",
(invite.fingerprint().hex(),),
)
.await?;
// Now start the protocol and initialise the state.
{
let has_key = context
.sql
.exists(
"SELECT COUNT(*) FROM public_keys WHERE fingerprint=?",
(invite.fingerprint().hex(),),
)
.await?;
// `joining_chat_id` is `Some` if group chat
// already exists and we are in the chat.
let joining_chat_id = match invite {
@@ -94,7 +86,7 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
// Even if Alice is not verified, we don't send anything.
context.emit_event(EventType::SecurejoinJoinerProgress {
contact_id: invite.contact_id(),
progress: JoinerProgress::Succeeded.to_usize(),
progress: JoinerProgress::Succeeded.into_u16(),
});
return Ok(joining_chat_id);
} else if has_key
@@ -113,7 +105,7 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
context.emit_event(EventType::SecurejoinJoinerProgress {
contact_id: invite.contact_id(),
progress: JoinerProgress::RequestWithAuthSent.to_usize(),
progress: JoinerProgress::RequestWithAuthSent.into_u16(),
});
} else {
send_handshake_message(context, &invite, private_chat_id, BobHandshakeMsg::Request)
@@ -152,20 +144,22 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
Ok(joining_chat_id)
}
QrInvite::Contact { .. } => {
// For setup-contact the BobState already ensured the 1:1 chat exists because it
// uses it to send the handshake messages.
chat::add_info_msg_with_cmd(
context,
private_chat_id,
&stock_str::securejoin_wait(context).await,
SystemMessage::SecurejoinWait,
None,
time(),
None,
None,
None,
)
.await?;
// For setup-contact the BobState already ensured the 1:1 chat exists because it is
// used to send the handshake messages.
if !has_key {
chat::add_info_msg_with_cmd(
context,
private_chat_id,
&stock_str::securejoin_wait(context).await,
SystemMessage::SecurejoinWait,
None,
time(),
None,
None,
None,
)
.await?;
}
Ok(private_chat_id)
}
}
@@ -175,6 +169,9 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
///
/// Returns the ID of the newly inserted entry.
async fn insert_new_db_entry(context: &Context, invite: QrInvite, chat_id: ChatId) -> Result<i64> {
// The `chat_id` isn't actually needed anymore,
// but we still save it;
// can be removed as a future improvement.
context
.sql
.insert(
@@ -184,6 +181,38 @@ async fn insert_new_db_entry(context: &Context, invite: QrInvite, chat_id: ChatI
.await
}
async fn delete_securejoin_wait_msg(context: &Context, chat_id: ChatId) -> Result<()> {
if let Some((msg_id, param)) = context
.sql
.query_row_optional(
"
SELECT id, param FROM msgs
WHERE timestamp=(SELECT MAX(timestamp) FROM msgs WHERE chat_id=? AND hidden=0)
AND chat_id=? AND hidden=0
LIMIT 1
",
(chat_id, chat_id),
|row| {
let id: MsgId = row.get(0)?;
let param: String = row.get(1)?;
let param: Params = param.parse().unwrap_or_default();
Ok((id, param))
},
)
.await?
&& param.get_cmd() == SystemMessage::SecurejoinWait
{
let on_server = false;
msg_id.trash(context, on_server).await?;
context.emit_event(EventType::MsgDeleted { chat_id, msg_id });
context.emit_msgs_changed_without_msg_id(chat_id);
chatlist_events::emit_chatlist_item_changed(context, chat_id);
context.emit_msgs_changed_without_ids();
chatlist_events::emit_chatlist_changed(context);
}
Ok(())
}
/// Handles `vc-auth-required` and `vg-auth-required` handshake messages.
///
/// # Bob - the joiner's side
@@ -195,11 +224,10 @@ pub(super) async fn handle_auth_required(
// Load all Bob states that expect `vc-auth-required` or `vg-auth-required`.
let bob_states = context
.sql
.query_map_vec("SELECT id, invite, chat_id FROM bobstate", (), |row| {
.query_map_vec("SELECT id, invite FROM bobstate", (), |row| {
let row_id: i64 = row.get(0)?;
let invite: QrInvite = row.get(1)?;
let chat_id: ChatId = row.get(2)?;
Ok((row_id, invite, chat_id))
Ok((row_id, invite))
})
.await?;
@@ -209,7 +237,7 @@ pub(super) async fn handle_auth_required(
);
let mut auth_sent = false;
for (bobstate_row_id, invite, chat_id) in bob_states {
for (bobstate_row_id, invite) in bob_states {
if !encrypted_and_signed(context, message, invite.fingerprint()) {
continue;
}
@@ -220,6 +248,12 @@ pub(super) async fn handle_auth_required(
}
info!(context, "Fingerprint verified.",);
let chat_id = private_chat_id(context, &invite).await?;
delete_securejoin_wait_msg(context, chat_id)
.await
.context("delete_securejoin_wait_msg")
.log_err(context)
.ok();
send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::RequestWithAuth).await?;
context
.sql
@@ -240,7 +274,7 @@ pub(super) async fn handle_auth_required(
context.emit_event(EventType::SecurejoinJoinerProgress {
contact_id: invite.contact_id(),
progress: JoinerProgress::RequestWithAuthSent.to_usize(),
progress: JoinerProgress::RequestWithAuthSent.into_u16(),
});
auth_sent = true;
@@ -348,6 +382,22 @@ impl BobHandshakeMsg {
}
}
/// Returns the 1:1 chat with the inviter.
///
/// This is the chat in which securejoin messages are sent.
/// The 1:1 chat will be created if it does not yet exist.
async fn private_chat_id(context: &Context, invite: &QrInvite) -> Result<ChatId> {
let hidden = match invite {
QrInvite::Contact { .. } => Blocked::Not,
QrInvite::Group { .. } => Blocked::Yes,
QrInvite::Broadcast { .. } => Blocked::Yes,
};
ChatId::create_for_contact_with_blocked(context, invite.contact_id(), hidden)
.await
.with_context(|| format!("can't create chat for contact {}", invite.contact_id()))
}
/// Returns the [`ChatId`] of the chat being joined.
///
/// This is the chat in which you want to notify the user as well.
@@ -406,8 +456,7 @@ pub(crate) enum JoinerProgress {
}
impl JoinerProgress {
#[expect(clippy::wrong_self_convention)]
pub(crate) fn to_usize(self) -> usize {
pub(crate) fn into_u16(self) -> u16 {
match self {
JoinerProgress::RequestWithAuthSent => 400,
JoinerProgress::Succeeded => 1000,

View File

@@ -100,6 +100,17 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
bob_chat.why_cant_send(&bob).await.unwrap(),
Some(CantSendReason::MissingKey)
);
// Check Bob's info messages.
let msg_cnt = 2;
let mut i = 0..msg_cnt;
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
assert!(msg.is_info());
assert_eq!(msg.get_text(), messages_e2e_encrypted(&bob).await);
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
assert!(msg.is_info());
assert_eq!(msg.get_text(), stock_str::securejoin_wait(&bob).await);
let contact_alice_id = bob.add_or_lookup_contact_no_key(&alice).await.id;
let sent = bob.pop_sent_msg().await;
assert!(!sent.payload.contains("Bob Examplenet"));
@@ -243,7 +254,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
.unwrap();
match case {
SetupContactCase::AliceHasName => assert_eq!(contact_alice.get_authname(), "Alice"),
_ => assert_eq!(contact_alice.get_authname(), "Alice Exampleorg"),
_ => assert_eq!(contact_alice.get_authname(), ""),
};
// Check Alice sent the right message to Bob.
@@ -272,15 +283,10 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
assert!(contact_alice.get_name().is_empty());
assert_eq!(contact_alice.is_bot(), case == SetupContactCase::AliceIsBot);
// Check Bob got expected info messages in his 1:1 chat.
let msg_cnt = 2;
let mut i = 0..msg_cnt;
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
// The `SecurejoinWait` info message has been removed, but the e2ee notice remains.
let msg = get_chat_msg(&bob, bob_chat.get_id(), 0, 1).await;
assert!(msg.is_info());
assert_eq!(msg.get_text(), messages_e2e_encrypted(&bob).await);
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
assert!(msg.is_info());
assert_eq!(msg.get_text(), stock_str::securejoin_wait(&bob).await);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -1013,7 +1019,7 @@ async fn test_expired_synced_auth_token() -> Result<()> {
let qr = get_securejoin_qr(alice2, None).await?;
alice2.send_sync_msg().await.unwrap();
let sync_msg = alice2.pop_sent_sync_msg().await;
let sync_msg = alice2.pop_sent_msg().await;
// One week passes, QR code expires.
SystemTime::shift(Duration::from_secs(7 * 24 * 3600));
@@ -1217,3 +1223,33 @@ async fn test_qr_no_implicit_inviter_addition() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_user_deletes_chat_before_securejoin_completes() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let qr = get_securejoin_qr(alice, None).await?;
let bob_chat_id = join_securejoin(bob, &qr).await?;
let bob_alice_chat = bob.get_chat(alice).await;
// It's not possible yet to send to the chat, because Bob doesn't have Alice's key:
assert_eq!(bob_alice_chat.can_send(bob).await?, false);
assert_eq!(bob_alice_chat.id, bob_chat_id);
let request = bob.pop_sent_msg().await;
bob_chat_id.delete(bob).await?;
alice.recv_msg_trash(&request).await;
let auth_required = alice.pop_sent_msg().await;
bob.recv_msg_trash(&auth_required).await;
// The chat with Alice should be recreated,
// and it should be sendable now:
assert!(bob.get_chat(alice).await.can_send(bob).await?);
Ok(())
}

View File

@@ -495,6 +495,7 @@ async fn send_mdns(context: &Context, connection: &mut Smtp) -> Result<()> {
pub(crate) async fn send_smtp_messages(context: &Context, connection: &mut Smtp) -> Result<()> {
let ratelimited = if context.ratelimit.read().await.can_send() {
// add status updates and sync messages to end of sending queue
context.send_sync_msg().await?;
context.flush_status_updates().await?;
false
} else {

View File

@@ -1011,6 +1011,8 @@ CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
inc_and_check(&mut migration_version, 119)?;
if dbversion < migration_version {
// This table is deprecated sinc 2025-12-25.
// Sync messages are again sent over SMTP.
sql.execute_migration(
"CREATE TABLE imap_send (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -1439,6 +1441,64 @@ CREATE INDEX imap_sync_index ON imap_sync(transport_id, folder);
.await?;
}
inc_and_check(&mut migration_version, 142)?;
if dbversion < migration_version {
sql.execute_migration(
"ALTER TABLE transports
ADD COLUMN add_timestamp INTEGER NOT NULL DEFAULT 0;
CREATE TABLE removed_transports (
addr TEXT NOT NULL,
remove_timestamp INTEGER NOT NULL,
UNIQUE(addr)
) STRICT;",
migration_version,
)
.await?;
}
inc_and_check(&mut migration_version, 143)?;
if dbversion < migration_version {
sql.execute_migration(
"
ALTER TABLE chats ADD COLUMN name_normalized TEXT;
ALTER TABLE contacts ADD COLUMN name_normalized TEXT;
",
migration_version,
)
.await?;
}
inc_and_check(&mut migration_version, 144)?;
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';
UPDATE config SET value='0' WHERE keyname='mvbox_move'",
)?;
}
Ok(())
},
migration_version,
)
.await?;
}
let new_version = sql
.get_raw_config_int(VERSION_CFG)
.await?

View File

@@ -160,9 +160,7 @@ async fn test_key_contacts_migration_verified() -> Result<()> {
"#,
)?)).await?;
STOP_MIGRATIONS_AT
.scope(133, t.sql.run_migrations(&t))
.await?;
t.sql.run_migrations(&t).await?;
// Hidden address-contact can't be looked up.
assert!(

View File

@@ -14,9 +14,9 @@ use serde::Serialize;
use crate::chat::{self, ChatId, MuteDuration};
use crate::config::Config;
use crate::constants::Chattype;
use crate::constants::{Chattype, DC_VERSION_STR};
use crate::contact::{Contact, ContactId, Origin, import_vcard, mark_contact_id_as_verified};
use crate::context::{Context, get_version_str};
use crate::context::Context;
use crate::key::load_self_public_keyring;
use crate::log::LogExt;
use crate::message::{Message, Viewtype};
@@ -25,8 +25,8 @@ use crate::tools::{create_id, time};
pub(crate) const STATISTICS_BOT_EMAIL: &str = "self_reporting@testrun.org";
const STATISTICS_BOT_VCARD: &str = include_str!("../assets/statistics-bot.vcf");
// const SENDING_INTERVAL_SECONDS: i64 = 3600 * 24 * 7; // 1 week
const SENDING_INTERVAL_SECONDS: i64 = 60; // 1 minute (for testing)
const SENDING_INTERVAL_SECONDS: i64 = 3600 * 24 * 7; // 1 week
// const SENDING_INTERVAL_SECONDS: i64 = 60; // 1 minute (for testing)
const MESSAGE_STATS_UPDATE_INTERVAL_SECONDS: i64 = 4 * 60; // 4 minutes (less than the lowest ephemeral messages timeout)
#[derive(Serialize)]
@@ -356,7 +356,7 @@ async fn get_stats(context: &Context) -> Result<String> {
get_timestamps(context, "stats_sending_disabled_events").await?;
let stats = Statistics {
core_version: get_version_str().to_string(),
core_version: DC_VERSION_STR.to_string(),
key_create_timestamps,
stats_id: stats_id(context).await?,
is_chatmail: context.is_chatmail().await?,

View File

@@ -46,7 +46,7 @@ async fn test_maybe_send_stats() -> Result<()> {
r.get("contact_stats").unwrap(),
&serde_json::Value::Array(vec![])
);
assert_eq!(r.get("core_version").unwrap(), get_version_str());
assert_eq!(r.get("core_version").unwrap(), DC_VERSION_STR);
assert_eq!(maybe_send_stats(alice).await?, None);

View File

@@ -109,15 +109,15 @@ pub enum StockMessage {
))]
DeviceMessagesHint = 70,
#[strum(props(fallback = "Welcome to Delta Chat! \
Delta Chat looks and feels like other popular messenger apps, \
but does not involve centralized control, \
tracking or selling you, friends, colleagues or family out to large organizations.\n\n\
Technically, Delta Chat is an email application with a modern chat interface. \
Email in a new dress if you will 👻\n\n\
Use Delta Chat with anyone out of billions of people: just use their e-mail address. \
Recipients don't need to install Delta Chat, visit websites or sign up anywhere - \
however, of course, if they like, you may point them to 👉 https://get.delta.chat"))]
#[strum(props(fallback = "Get in contact!\n\n\
🙌 Tap \"QR code\" on the main screen of both devices. \
Choose \"Scan QR Code\" on one device, and point it at the other\n\n\
🌍 If not in the same room, \
scan via video call or share an invite link from \"Scan QR code\"\n\n\
Then: Enjoy your decentralized messenger experience. \
In contrast to other popular apps, \
without central control or tracking or selling you, \
friends, colleagues or family out to large organizations."))]
WelcomeMessage = 71,
#[strum(props(fallback = "Message from %1$s"))]
@@ -1144,14 +1144,6 @@ pub(crate) async fn outgoing_messages(context: &Context) -> String {
translated(context, StockMessage::OutgoingMessages).await
}
/// Stock string: `Storage on %1$s`.
/// `%1$s` will be replaced by the domain of the configured email-address.
pub(crate) async fn storage_on_domain(context: &Context, domain: &str) -> String {
translated(context, StockMessage::StorageOnDomain)
.await
.replace1(domain)
}
/// Stock string: `Not connected`.
pub(crate) async fn not_connected(context: &Context) -> String {
translated(context, StockMessage::NotConnected).await

View File

@@ -2,23 +2,28 @@
use crate::{context::Context, message::MsgId};
use anyhow::Result;
use humansize::{BINARY, format_size};
use walkdir::WalkDir;
/// Storage Usage Report
/// Useful for debugging space usage problems in the deltachat database.
#[derive(Debug)]
pub struct StorageUsage {
/// Total database size, subtract this from the backup size to estimate size of all blobs
pub db_size: usize,
pub db_size: u64,
/// size and row count of the 10 biggest tables
pub largest_tables: Vec<(String, usize, Option<usize>)>,
pub largest_tables: Vec<(String, u64, Option<u64>)>,
/// count and total size of status updates
/// for the 10 webxdc apps with the most size usage in status updates
pub largest_webxdc_data: Vec<(MsgId, usize, usize)>,
pub largest_webxdc_data: Vec<(MsgId, u64, u64)>,
/// Total size of all files in the blobdir
pub blobdir_size: u64,
}
impl std::fmt::Display for StorageUsage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Storage Usage:")?;
let blobdir_size = format_size(self.blobdir_size, BINARY);
writeln!(f, "[Blob Directory Size]: {blobdir_size}")?;
let human_db_size = format_size(self.db_size, BINARY);
writeln!(f, "[Database Size]: {human_db_size}")?;
writeln!(f, "[Largest Tables]:")?;
@@ -46,12 +51,16 @@ impl std::fmt::Display for StorageUsage {
/// Get storage usage information for the Context's database
pub async fn get_storage_usage(ctx: &Context) -> Result<StorageUsage> {
let page_size: usize = ctx
let context_clone = ctx.clone();
let blobdir_size =
tokio::task::spawn_blocking(move || get_blobdir_storage_usage(&context_clone));
let page_size: u64 = ctx
.sql
.query_get_value("PRAGMA page_size", ())
.await?
.unwrap_or_default();
let page_count: usize = ctx
let page_count: u64 = ctx
.sql
.query_get_value("PRAGMA page_count", ())
.await?
@@ -68,7 +77,7 @@ pub async fn get_storage_usage(ctx: &Context) -> Result<StorageUsage> {
(),
|row| {
let name: String = row.get(0)?;
let size: usize = row.get(1)?;
let size: u64 = row.get(1)?;
Ok((name, size, None))
},
)
@@ -76,7 +85,7 @@ pub async fn get_storage_usage(ctx: &Context) -> Result<StorageUsage> {
for row in &mut largest_tables {
let name = &row.0;
let row_count: Result<Option<usize>> = ctx
let row_count: Result<Option<u64>> = ctx
.sql
// SECURITY: the table name comes from the db, not from the user
.query_get_value(&format!("SELECT COUNT(*) FROM {name}"), ())
@@ -93,17 +102,31 @@ pub async fn get_storage_usage(ctx: &Context) -> Result<StorageUsage> {
(),
|row| {
let msg_id: MsgId = row.get(0)?;
let size: usize = row.get(1)?;
let count: usize = row.get(2)?;
let size: u64 = row.get(1)?;
let count: u64 = row.get(2)?;
Ok((msg_id, size, count))
},
)
.await?;
let blobdir_size = blobdir_size.await?;
Ok(StorageUsage {
db_size: page_size * page_count,
largest_tables,
largest_webxdc_data,
blobdir_size,
})
}
/// Returns storage usage of the blob directory
pub fn get_blobdir_storage_usage(ctx: &Context) -> u64 {
WalkDir::new(ctx.get_blobdir())
.max_depth(2)
.into_iter()
.filter_map(|entry| entry.ok())
.filter_map(|entry| entry.metadata().ok())
.filter(|metadata| metadata.is_file())
.fold(0, |acc, m| acc + m.len())
}

View File

@@ -9,14 +9,15 @@ use crate::config::Config;
use crate::constants::Blocked;
use crate::contact::ContactId;
use crate::context::Context;
use crate::log::LogExt;
use crate::log::warn;
use crate::log::{LogExt as _, warn};
use crate::login_param::EnteredLoginParam;
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::param::Param;
use crate::sync::SyncData::{AddQrToken, AlterChat, DeleteQrToken};
use crate::token::Namespace;
use crate::tools::time;
use crate::transport::{ConfiguredLoginParamJson, sync_transports};
use crate::{message, stock_str, token};
use std::collections::HashSet;
@@ -52,6 +53,29 @@ pub(crate) struct QrTokenData {
pub(crate) grpid: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct TransportData {
/// Configured login parameters.
pub(crate) configured: ConfiguredLoginParamJson,
/// Login parameters entered by the user.
///
/// They can be used to reconfigure the transport.
pub(crate) entered: EnteredLoginParam,
/// Timestamp of when the transport was last time (re)configured.
pub(crate) timestamp: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct RemovedTransportData {
/// Address of the removed transport.
pub(crate) addr: String,
/// Timestamp of when the transport was removed.
pub(crate) timestamp: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) enum SyncData {
AddQrToken(QrTokenData),
@@ -71,6 +95,28 @@ pub(crate) enum SyncData {
DeleteMessages {
msgs: Vec<String>, // RFC724 id (i.e. "Message-Id" header)
},
/// Update transport configuration.
///
/// This message contains a list of all added transports
/// together with their addition timestamp,
/// and all removed transports together with
/// the removal timestamp.
///
/// In case of a tie, addition and removal timestamps
/// being the same, removal wins.
/// It is more likely that transport is added
/// and then removed within a second,
/// but unlikely the other way round
/// as adding new transport takes time
/// to run configuration.
Transports {
/// Active transports.
transports: Vec<TransportData>,
/// Removed transports with the timestamp of removal.
removed_transports: Vec<RemovedTransportData>,
},
}
#[derive(Debug, Serialize, Deserialize)]
@@ -129,7 +175,7 @@ impl Context {
/// Adds most recent qr-code tokens for the given group or self-contact to the list of items to
/// be synced. If device synchronization is disabled,
/// no tokens exist or the chat is unpromoted, the function does nothing.
/// The caller should call `SchedulerState::interrupt_inbox()` on its own to trigger sending.
/// The caller should call `SchedulerState::interrupt_smtp()` on its own to trigger sending.
pub(crate) async fn sync_qr_code_tokens(&self, grpid: Option<&str>) -> Result<()> {
if !self.should_send_sync_msgs().await? {
return Ok(());
@@ -162,7 +208,7 @@ impl Context {
grpid: None,
}))
.await?;
self.scheduler.interrupt_inbox().await;
self.scheduler.interrupt_smtp().await;
Ok(())
}
@@ -274,6 +320,10 @@ impl Context {
SyncData::Config { key, val } => self.sync_config(key, val).await,
SyncData::SaveMessage { src, dest } => self.save_message(src, dest).await,
SyncData::DeleteMessages { msgs } => self.sync_message_deletion(msgs).await,
SyncData::Transports {
transports,
removed_transports,
} => sync_transports(self, transports, removed_transports).await,
},
SyncDataOrUnknown::Unknown(data) => {
warn!(self, "Ignored unknown sync item: {data}.");
@@ -625,7 +675,7 @@ mod tests {
// let alice's other device receive and execute the sync message,
// also here, self-talk should stay hidden
let sent_msg = alice.pop_sent_sync_msg().await;
let sent_msg = alice.pop_sent_msg().await;
let alice2 = TestContext::new_alice().await;
alice2.set_config_bool(Config::SyncMsgs, true).await?;
alice2.recv_msg_trash(&sent_msg).await;
@@ -672,7 +722,7 @@ mod tests {
}))
.await?;
alice1.send_sync_msg().await?.unwrap();
alice1.pop_sent_sync_msg().await
alice1.pop_sent_msg().await
} else {
let chat = alice1.get_self_chat().await;
alice1.send_text(chat.id, "Hi").await
@@ -710,7 +760,7 @@ mod tests {
.set_config(Config::Displayname, Some("Alice Human"))
.await?;
alice.send_sync_msg().await?;
alice.pop_sent_sync_msg().await;
alice.pop_sent_msg().await;
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
assert_eq!(msg.text, "hi");
@@ -744,7 +794,7 @@ mod tests {
// group is promoted for compatibility (because the group could be created by older Core).
// TODO: assert!(msg_id.is_none());
assert!(msg_id.is_some());
let sent = alice.pop_sent_sync_msg().await;
let sent = alice.pop_sent_msg().await;
let msg = alice.parse_msg(&sent).await;
let mut sync_items = msg.sync_items.unwrap().items;
assert_eq!(sync_items.len(), 1);

View File

@@ -600,7 +600,7 @@ impl TestContext {
self.ctx
.set_config(Config::ConfiguredAddr, Some(addr))
.await
.unwrap();
.expect("Failed to configure address");
if let Some(name) = addr.split('@').next() {
self.set_name(name);
@@ -711,46 +711,6 @@ impl TestContext {
})
}
/// Retrieves a sent sync message from the db.
///
/// This retrieves and removes a sync message which has been scheduled to send from the jobs
/// table. Messages are returned in the order they have been sent.
///
/// Panics if there is no message or on any error.
pub async fn pop_sent_sync_msg(&self) -> SentMessage<'_> {
let (id, msg_id, payload) = self
.ctx
.sql
.query_row(
"SELECT id, msg_id, mime \
FROM imap_send \
ORDER BY id",
(),
|row| {
let rowid: i64 = row.get(0)?;
let msg_id: MsgId = row.get(1)?;
let mime: String = row.get(2)?;
Ok((rowid, msg_id, mime))
},
)
.await
.expect("query_row failed");
self.ctx
.sql
.execute("DELETE FROM imap_send WHERE id=?", (id,))
.await
.expect("failed to remove job");
update_msg_state(&self.ctx, msg_id, MessageState::OutDelivered)
.await
.expect("failed to update message state");
SentMessage {
payload,
sender_msg_id: msg_id,
sender_context: &self.ctx,
recipients: self.get_primary_self_addr().await.unwrap(),
}
}
/// Parses a message.
///
/// Parsing a message does not run the entire receive pipeline, but is not without
@@ -896,6 +856,15 @@ impl TestContext {
/// If the contact does not exist yet, a new contact will be created
/// with the correct fingerprint, but without the public key.
pub async fn add_or_lookup_contact_no_key(&self, other: &TestContext) -> Contact {
let contact_id = self.add_or_lookup_contact_id_no_key(other).await;
Contact::get_by_id(&self.ctx, contact_id).await.unwrap()
}
/// Returns the [`ContactId`] for the other [`TestContext`], creating it if necessary.
///
/// If the contact does not exist yet, a new contact will be created
/// with the correct fingerprint, but without the public key.
async fn add_or_lookup_contact_id_no_key(&self, other: &TestContext) -> ContactId {
let primary_self_addr = other.ctx.get_primary_self_addr().await.unwrap();
let addr = ContactAddress::new(&primary_self_addr).unwrap();
let fingerprint = self_fingerprint(other).await.unwrap();
@@ -904,7 +873,7 @@ impl TestContext {
Contact::add_or_lookup_ex(self, "", &addr, fingerprint, Origin::MailinglistAddress)
.await
.expect("add_or_lookup");
Contact::get_by_id(&self.ctx, contact_id).await.unwrap()
contact_id
}
/// Returns 1:1 [`Chat`] with another account address-contact.
@@ -935,7 +904,7 @@ impl TestContext {
/// so may create a key-contact with a fingerprint
/// but without the key.
pub async fn get_chat(&self, other: &TestContext) -> Chat {
let contact = self.add_or_lookup_contact_id(other).await;
let contact = self.add_or_lookup_contact_id_no_key(other).await;
let chat_id = ChatIdBlocked::lookup_by_contact(&self.ctx, contact)
.await
@@ -1465,13 +1434,14 @@ impl EventTracker {
event_matcher: F,
) -> Option<EventType> {
ctx.emit_event(EventType::Test);
let mut found_event = None;
loop {
let event = self.recv().await.unwrap();
if event_matcher(&event.typ) {
return Some(event.typ);
}
if let EventType::Test = event.typ {
return None;
return found_event;
}
if event_matcher(&event.typ) {
found_event = Some(event.typ);
}
}
}
@@ -1541,7 +1511,7 @@ pub(crate) async fn mark_as_verified(this: &TestContext, other: &TestContext) {
/// alice0's side that implies sending a sync message.
pub(crate) async fn sync(alice0: &TestContext, alice1: &TestContext) {
alice0.send_sync_msg().await.unwrap();
let sync_msg = alice0.pop_sent_sync_msg().await;
let sync_msg = alice0.pop_sent_msg().await;
alice1.recv_msg_trash(&sync_msg).await;
}

View File

@@ -779,6 +779,15 @@ pub(crate) fn to_lowercase(s: &str) -> Cow<'_, str> {
}
}
/// Returns text for storing in special db columns to make case-insensitive search possible for
/// non-ASCII messages, chat and contact names.
pub(crate) fn normalize_text(text: &str) -> Option<String> {
if text.is_ascii() {
return None;
};
Some(text.to_lowercase()).filter(|t| t != text)
}
/// Increments `*t` and checks that it equals to `expected` after that.
pub(crate) fn inc_and_check<T: PrimInt + AddAssign + std::fmt::Debug>(
t: &mut T,
@@ -789,6 +798,26 @@ pub(crate) fn inc_and_check<T: PrimInt + AddAssign + std::fmt::Debug>(
Ok(())
}
/// Converts usize to u64 without using `as`.
///
/// This is needed for example to convert in-memory buffer sizes
/// to u64 type used for counting all the bytes written.
///
/// On 32-bit systems it is possible to have files
/// larger than 4 GiB or write more than 4 GiB to network connection,
/// in which case we need a 64-bit total counter,
/// but use 32-bit usize for buffer sizes.
///
/// This can only break if usize has more than 64 bits
/// and this is not the case as of 2025 and is
/// unlikely to change for general purpose computers.
/// See <https://github.com/rust-lang/rust/issues/30495>
/// and <https://users.rust-lang.org/t/cant-convert-usize-to-u64/6243>
/// and <https://github.com/rust-lang/rust/issues/106050>.
pub(crate) fn usize_to_u64(v: usize) -> u64 {
u64::try_from(v).unwrap_or(u64::MAX)
}
/// Returns early with an error if a condition is not satisfied.
/// In non-optimized builds, panics instead if so.
#[macro_export]

View File

@@ -18,10 +18,12 @@ use crate::config::Config;
use crate::configure::server_params::{ServerParams, expand_param_vector};
use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2};
use crate::context::Context;
use crate::events::EventType;
use crate::login_param::EnteredLoginParam;
use crate::net::load_connection_timestamp;
use crate::provider::{Protocol, Provider, Socket, UsernamePattern, get_provider_by_id};
use crate::sql::Sql;
use crate::sync::{RemovedTransportData, SyncData, TransportData};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) enum ConnectionSecurity {
@@ -190,10 +192,10 @@ pub(crate) struct ConfiguredLoginParam {
pub oauth2: bool,
}
/// The representation of ConfiguredLoginParam in the database,
/// saved as Json.
/// JSON representation of ConfiguredLoginParam
/// for the database and sync messages.
#[derive(Debug, Serialize, Deserialize)]
struct ConfiguredLoginParamJson {
pub(crate) struct ConfiguredLoginParamJson {
pub addr: String,
pub imap: Vec<ConfiguredServerLoginParam>,
pub imap_user: String,
@@ -557,35 +559,9 @@ impl ConfiguredLoginParam {
self,
context: &Context,
entered_param: &EnteredLoginParam,
timestamp: i64,
) -> Result<()> {
let addr = addr_normalize(&self.addr);
let provider_id = self.provider.map(|provider| provider.id);
let configured_addr = context.get_config(Config::ConfiguredAddr).await?;
context
.sql
.execute(
"INSERT INTO transports (addr, entered_param, configured_param)
VALUES (?, ?, ?)
ON CONFLICT (addr)
DO UPDATE SET entered_param=excluded.entered_param, configured_param=excluded.configured_param",
(
self.addr.clone(),
serde_json::to_string(entered_param)?,
self.into_json()?,
),
)
.await?;
if configured_addr.is_none() {
// If there is no transport yet, set the new transport as the primary one
context
.sql
.set_raw_config(Config::ConfiguredProvider.as_ref(), provider_id)
.await?;
context
.sql
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr))
.await?;
}
save_transport(context, entered_param, &self.into(), timestamp).await?;
Ok(())
}
@@ -609,18 +585,7 @@ impl ConfiguredLoginParam {
}
pub(crate) fn into_json(self) -> Result<String> {
let json = ConfiguredLoginParamJson {
addr: self.addr,
imap: self.imap,
imap_user: self.imap_user,
imap_password: self.imap_password,
smtp: self.smtp,
smtp_user: self.smtp_user,
smtp_password: self.smtp_password,
provider_id: self.provider.map(|p| p.id.to_string()),
certificate_checks: self.certificate_checks,
oauth2: self.oauth2,
};
let json: ConfiguredLoginParamJson = self.into();
Ok(serde_json::to_string(&json)?)
}
@@ -638,12 +603,181 @@ impl ConfiguredLoginParam {
}
}
impl From<ConfiguredLoginParam> for ConfiguredLoginParamJson {
fn from(configured_login_param: ConfiguredLoginParam) -> Self {
Self {
addr: configured_login_param.addr,
imap: configured_login_param.imap,
imap_user: configured_login_param.imap_user,
imap_password: configured_login_param.imap_password,
smtp: configured_login_param.smtp,
smtp_user: configured_login_param.smtp_user,
smtp_password: configured_login_param.smtp_password,
provider_id: configured_login_param.provider.map(|p| p.id.to_string()),
certificate_checks: configured_login_param.certificate_checks,
oauth2: configured_login_param.oauth2,
}
}
}
/// Saves transport to the database.
pub(crate) async fn save_transport(
context: &Context,
entered_param: &EnteredLoginParam,
configured: &ConfiguredLoginParamJson,
add_timestamp: i64,
) -> Result<()> {
let addr = addr_normalize(&configured.addr);
let configured_addr = context.get_config(Config::ConfiguredAddr).await?;
context
.sql
.execute(
"INSERT INTO transports (addr, entered_param, configured_param, add_timestamp)
VALUES (?, ?, ?, ?)
ON CONFLICT (addr)
DO UPDATE SET entered_param=excluded.entered_param,
configured_param=excluded.configured_param,
add_timestamp=excluded.add_timestamp",
(
&addr,
serde_json::to_string(entered_param)?,
serde_json::to_string(configured)?,
add_timestamp,
),
)
.await?;
if configured_addr.is_none() {
// If there is no transport yet, set the new transport as the primary one
context
.sql
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr))
.await?;
}
Ok(())
}
/// Sends a sync message to synchronize transports across devices.
pub(crate) async fn send_sync_transports(context: &Context) -> Result<()> {
info!(context, "Sending transport synchronization message.");
// Synchronize all transport configurations.
//
// Transport with ID 1 is never synchronized
// because it can only be created during initial configuration.
// This also guarantees that credentials for the first
// transport are never sent in sync messages,
// so this is not worse than when not using multi-transport.
// If transport ID 1 is reconfigured,
// likely because the password has changed,
// user has to reconfigure it manually on all devices.
let transports = context
.sql
.query_map_vec(
"SELECT entered_param, configured_param, add_timestamp
FROM transports WHERE id>1",
(),
|row| {
let entered_json: String = row.get(0)?;
let entered: EnteredLoginParam = serde_json::from_str(&entered_json)?;
let configured_json: String = row.get(1)?;
let configured: ConfiguredLoginParamJson = serde_json::from_str(&configured_json)?;
let timestamp: i64 = row.get(2)?;
Ok(TransportData {
configured,
entered,
timestamp,
})
},
)
.await?;
let removed_transports = context
.sql
.query_map_vec(
"SELECT addr, remove_timestamp FROM removed_transports",
(),
|row| {
let addr: String = row.get(0)?;
let timestamp: i64 = row.get(1)?;
Ok(RemovedTransportData { addr, timestamp })
},
)
.await?;
context
.add_sync_item(SyncData::Transports {
transports,
removed_transports,
})
.await?;
context.scheduler.interrupt_smtp().await;
Ok(())
}
/// Process received data for transport synchronization.
pub(crate) async fn sync_transports(
context: &Context,
transports: &[TransportData],
removed_transports: &[RemovedTransportData],
) -> Result<()> {
for TransportData {
configured,
entered,
timestamp,
} in transports
{
save_transport(context, entered, configured, *timestamp).await?;
}
context
.sql
.transaction(|transaction| {
for RemovedTransportData { addr, timestamp } in removed_transports {
transaction.execute(
"DELETE FROM transports
WHERE addr=? AND add_timestamp<=?",
(addr, timestamp),
)?;
transaction.execute(
"INSERT INTO removed_transports (addr, remove_timestamp)
VALUES (?, ?)
ON CONFLICT (addr) DO
UPDATE SET remove_timestamp = excluded.remove_timestamp
WHERE excluded.remove_timestamp > remove_timestamp",
(addr, timestamp),
)?;
}
Ok(())
})
.await?;
context.emit_event(EventType::TransportsModified);
Ok(())
}
/// Adds transport entry to the `transports` table with empty configuration.
pub(crate) async fn add_pseudo_transport(context: &Context, addr: &str) -> Result<()> {
context.sql
.execute(
"INSERT INTO transports (addr, entered_param, configured_param) VALUES (?, ?, ?)",
(
addr,
serde_json::to_string(&EnteredLoginParam::default())?,
format!(r#"{{"addr":"{addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#)
),
)
.await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::log::LogExt as _;
use crate::provider::get_provider_by_id;
use crate::test_utils::TestContext;
use crate::tools::time;
#[test]
fn test_configured_certificate_checks_display() {
@@ -688,7 +822,7 @@ mod tests {
param
.clone()
.save_to_transports_table(&t, &EnteredLoginParam::default())
.save_to_transports_table(&t, &EnteredLoginParam::default(), time())
.await?;
let expected_param = r#"{"addr":"alice@example.org","imap":[{"connection":{"host":"imap.example.com","port":123,"security":"Starttls"},"user":"alice"}],"imap_user":"","imap_password":"foo","smtp":[{"connection":{"host":"smtp.example.com","port":456,"security":"Tls"},"user":"alice@example.org"}],"smtp_user":"","smtp_password":"bar","provider_id":null,"certificate_checks":"Strict","oauth2":false}"#;
assert_eq!(
@@ -906,7 +1040,7 @@ mod tests {
certificate_checks: ConfiguredCertificateChecks::Automatic,
oauth2: false,
}
.save_to_transports_table(&t, &EnteredLoginParam::default())
.save_to_transports_table(&t, &EnteredLoginParam::default(), time())
.await?;
let (_transport_id, loaded) = ConfiguredLoginParam::load(&t).await?.unwrap();

View File

@@ -411,8 +411,16 @@ impl Context {
&& let Some(notify_list) = status_update_item.notify
{
let self_addr = instance.get_webxdc_self_addr(self).await?;
if let Some(notify_text) = notify_list.get(&self_addr).or_else(|| notify_list.get("*"))
let notify_text = if let Some(notify_text) = notify_list.get(&self_addr) {
Some(notify_text)
} else if let Some(notify_text) = notify_list.get("*")
&& !Chat::load_from_db(self, instance.chat_id).await?.is_muted()
{
Some(notify_text)
} else {
None
};
if let Some(notify_text) = notify_text {
self.emit_event(EventType::IncomingWebxdcNotify {
chat_id: instance.chat_id,
contact_id: from_id,

View File

@@ -5,8 +5,8 @@ use serde_json::json;
use super::*;
use crate::chat::{
ChatId, add_contact_to_chat, create_broadcast, create_group, forward_msgs,
remove_contact_from_chat, resend_msgs, send_msg, send_text_msg,
ChatId, MuteDuration, add_contact_to_chat, create_broadcast, create_group, forward_msgs,
remove_contact_from_chat, resend_msgs, send_msg, send_text_msg, set_muted,
};
use crate::chatlist::Chatlist;
use crate::config::Config;
@@ -2073,6 +2073,74 @@ async fn test_webxdc_notify_all() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_webxdc_notify_muted() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let fiona = tcm.fiona().await;
let grp_id = alice
.create_group_with_members("grp", &[&bob, &fiona])
.await;
let alice_instance = send_webxdc_instance(&alice, grp_id).await?;
let sent1 = alice.pop_sent_msg().await;
let bob_instance = bob.recv_msg(&sent1).await;
let fiona_instance = fiona.recv_msg(&sent1).await;
set_muted(&bob, bob_instance.chat_id, MuteDuration::Forever).await?;
alice
.send_webxdc_status_update(
alice_instance.id,
"{\"payload\":7,\"info\": \"all\", \"notify\":{\"*\":\"notify all\"} }",
)
.await?;
alice.flush_status_updates().await?;
let sent2 = alice.pop_sent_msg().await;
let info_msg = alice.get_last_msg().await;
assert_eq!(info_msg.text, "all");
assert!(!has_incoming_webxdc_event(&alice, info_msg, "notify all").await);
bob.recv_msg_trash(&sent2).await;
let info_msg = bob.get_last_msg().await;
assert_eq!(info_msg.text, "all");
assert!(!has_incoming_webxdc_event(&bob, info_msg, "notify all").await);
fiona.recv_msg_trash(&sent2).await;
let info_msg = fiona.get_last_msg().await;
assert_eq!(info_msg.text, "all");
assert!(has_incoming_webxdc_event(&fiona, info_msg, "notify all").await);
alice
.send_webxdc_status_update(
alice_instance.id,
&format!(
"{{\"payload\":8,\"info\": \"reply\", \"notify\":{{\"{}\":\"reply, Bob\",\"{}\":\"reply, Fiona\"}} }}",
bob_instance.get_webxdc_self_addr(&bob).await?,
fiona_instance.get_webxdc_self_addr(&fiona).await?
),
)
.await?;
alice.flush_status_updates().await?;
let sent3 = alice.pop_sent_msg().await;
let info_msg = alice.get_last_msg().await;
assert_eq!(info_msg.text, "reply");
assert!(!has_incoming_webxdc_event(&alice, info_msg, "").await);
bob.recv_msg_trash(&sent3).await;
let info_msg = bob.get_last_msg().await;
assert_eq!(info_msg.text, "reply");
assert!(has_incoming_webxdc_event(&bob, info_msg, "reply, Bob").await);
fiona.recv_msg_trash(&sent3).await;
let info_msg = fiona.get_last_msg().await;
assert_eq!(info_msg.text, "reply");
assert!(has_incoming_webxdc_event(&fiona, info_msg, "reply, Fiona").await);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_webxdc_notify_bob_and_all() -> Result<()> {
let mut tcm = TestContextManager::new();