Compare commits

...
Author SHA1 Message Date
Simon Laux aa5d02137a add addr to contactinfo in repl 2026-01-08 20:29:00 +01:00
Simon Laux 4f816ae6ec api!: remove contact get_name_n_addr (cffi: dc_contact_get_name_n_addr
and jsonrpc: `Contact.name_and_addr`)
2026-01-08 20:06:46 +01:00
Simon Laux eee624dcbd fix: do not show contact address in message info 2026-01-08 19:43:25 +01:00
Simon Laux fe6e942191 api: cffi api to create account manager with existing events channel to see events emitted during startup. dc_event_channel_new, dc_event_channel_unref, dc_event_channel_get_event_emitter and dc_accounts_new_with_event_channel (#7609)
closes #7606
2026-01-08 18:08:00 +00:00
B. Petersen 67aac12995 docs: update instructions to UI where to display the address 2026-01-08 17:19:54 +01:00
B. Petersen f2fb59f0cc test that channel summary does not have sender name 2026-01-08 17:17:16 +01:00
B. Petersen 55ab1b86f7 feat: more text instead of sender in channel summary
the sender is usually always the same and is not needed already in the summary;
making more place for the message instead
2026-01-08 17:17:16 +01:00
Hocuri ceba687df3 feat: Config option to skip seen synchronization (#7694)
At urgent request from @hpk42, this adds a config option `team_profile`.
This option is only settable via SQLite (not exposed in the UI), and the
only thing it does is disabling synchronization of seen status.

I tested manually on my Android phone that it works.

Not straigthforward to write an automatic test, because we want to test that something
does _not_ happen (i.e. that the seen status is _not_ synchronized), and
it's not clear how long to wait before we check.

Probably it's fine to just not add a test.

This is what I tried:

```python
@pytest.mark.parametrize("team_profile", [True, False])
def test_markseen_basic(team_profile, acfactory):
    """
    Test that seen status is synchronized iff `team_profile` isn't set.
    """
    alice, bob = acfactory.get_online_accounts(2)

    # Bob sets up a second device.
    bob2 = bob.clone()
    bob2.start_io()

    alice_chat_bob = alice.create_chat(bob)
    bob.create_chat(alice)
    bob2.create_chat(alice)
    alice_chat_bob.send_text("Hello Bob!")

    message = bob.wait_for_incoming_msg()
    message2 = bob2.wait_for_incoming_msg()
    assert message2.get_snapshot().state == MessageState.IN_FRESH

    message.mark_seen()

    # PROBLEM: We're not waiting 'long enough',
    # so, the 'state == MessageState.IN_SEEN' assertion below fails
    bob.create_chat(bob).send_text("Self-sent message")
    self_sent = bob2.wait_for_msg(EventType.MSGS_CHANGED)
    assert self_sent.get_snapshot().text == "Self-sent message"

    if team_profile:
        assert message2.get_snapshot().state == MessageState.IN_FRESH
    else:
        assert message2.get_snapshot().state == MessageState.IN_SEEN
```
2026-01-08 17:06:03 +01:00
link2xt 7e811469b3 api: add who_can_call_me config option 2026-01-07 22:00:54 +00:00
link2xt cdacad235e chore: update lru 0.12.3 to 0.12.5 and add RUSTSEC-2026-0002 exception
Closes https://github.com/chatmail/core/issues/7692
2026-01-07 20:18:32 +00:00
link2xt c766397abc test: regression test for vc-request encrypted by the server 2026-01-07 15:47:16 +00:00
link2xt 14a59afd5d fix: do not rely on Secure-Join header to detect {vc,vg}-request 2026-01-07 15:47:16 +00:00
dependabot[bot] 9c883e6424 chore(cargo): bump rsa from 0.9.9 to 0.9.10
Bumps [rsa](https://github.com/RustCrypto/RSA) from 0.9.9 to 0.9.10.
- [Changelog](https://github.com/RustCrypto/RSA/blob/v0.9.10/CHANGELOG.md)
- [Commits](https://github.com/RustCrypto/RSA/compare/v0.9.9...v0.9.10)

---
updated-dependencies:
- dependency-name: rsa
  dependency-version: 0.9.10
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-06 17:08:26 -03:00
Simon Laux 9d7db20225 refactor(ffi): replace implicit drop in cffi with explicit drop(Arc::from_raw(var)) (#7664)
for improved clarity and consistency with existing code.
2026-01-05 19:27:48 +00:00
Simon Lauxandiequidoo fdb583b5e9 api: jsonrpc api get_all_ui_config_keys to get all "ui.*" config keys (#7579)
Adds an api to get all ui config keys. There already is an option to get
all normal config keys (`"sys.config_keys"`), but before this pr there
was no way to get all `ui.*` config keys.

#### Why is this api needed?

For webxdc cleanup on desktop, which stores window position in a ui
config key (such as `ui.desktop.webxdcBounds.676464`) as soon as a
webxdc is opened since many versions now. So listing all ui keys is a
good way for us to find out which webxdc may have web data stored.
unfortunately electron does not (yet?) have a way to list all origins
that have web-data like android does, so this is the next best thing we
can do before itterating all possible ids, see also
https://github.com/deltachat/deltachat-desktop/issues/5758.

#### Why is this only a jsonrpc api and not another special/virtual
config key like `"sys.config_keys"`?

r10s indicated that `ui.*`-config keys are barely used
(https://github.com/deltachat/deltachat-desktop/issues/5790#issuecomment-3598512802),
so I thought it makes more sense to add it as dedicated api which's
existentence is checked by the typechecker, so it will be easier to not
miss it when we should remove the api again in the future.

But we could also do a dedicated special/virtual config key for it, if
you think that is better, this is easy to change.

---------

Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com>
2026-01-03 21:14:00 +01:00
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 Zandbergenandlink2xt 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](https://github.com/cachix/install-nix-action/compare/0b0e072294b088b73964f1d72dfdac0951439dbd...4e002c8ec80594ecd40e759629461e26c8abed15)

---
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](https://github.com/astral-sh/setup-uv/compare/1e862dfacbd1d6d858c55d9b792c756523627244...681c641aba71e4a1c380be3ab5e12ad51f415867)

---
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 Lauxandiequidoo 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
74 changed files with 1996 additions and 1007 deletions
+13 -13
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
+47 -47
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,7 +382,7 @@ 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
@@ -406,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
@@ -496,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
+1 -1
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
+3 -3
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 }}
@@ -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
+2 -2
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"
+2 -2
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
+1 -1
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
+136
View File
@@ -1,5 +1,138 @@
# 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
@@ -7371,3 +7504,6 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[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
Generated
+90 -84
View File
@@ -62,18 +62,6 @@ dependencies = [
"aes",
]
[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.3"
@@ -98,6 +86,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 +1039,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 +1052,7 @@ dependencies = [
"itertools",
"num-traits",
"oorandom",
"page_size",
"plotters",
"rayon",
"regex",
@@ -1066,9 +1065,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 +1303,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "2.33.0"
version = "2.36.0"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -1388,6 +1387,7 @@ dependencies = [
"tracing",
"url",
"uuid",
"walkdir",
"webpki-roots",
]
@@ -1413,7 +1413,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "2.33.0"
version = "2.36.0"
dependencies = [
"anyhow",
"async-channel 2.5.0",
@@ -1429,13 +1429,12 @@ dependencies = [
"tempfile",
"tokio",
"typescript-type-def",
"walkdir",
"yerpc",
]
[[package]]
name = "deltachat-repl"
version = "2.33.0"
version = "2.36.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1451,7 +1450,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "2.33.0"
version = "2.36.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1480,7 +1479,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "2.33.0"
version = "2.36.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1997,7 +1996,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",
]
@@ -2367,22 +2366,14 @@ dependencies = [
"crunchy",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
"allocator-api2",
]
[[package]]
name = "hashbrown"
version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
]
@@ -2392,7 +2383,7 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
dependencies = [
"hashbrown 0.15.4",
"hashbrown",
]
[[package]]
@@ -2648,9 +2639,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",
@@ -2909,7 +2900,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
dependencies = [
"equivalent",
"hashbrown 0.15.4",
"hashbrown",
]
[[package]]
@@ -3173,7 +3164,7 @@ dependencies = [
"iroh-metrics",
"iroh-quinn",
"iroh-quinn-proto",
"lru 0.12.3",
"lru 0.12.5",
"n0-future",
"num_enum",
"pin-project",
@@ -3267,9 +3258,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 +3308,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 +3335,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"
@@ -3363,11 +3354,11 @@ dependencies = [
[[package]]
name = "lru"
version = "0.12.3"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc"
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
dependencies = [
"hashbrown 0.14.5",
"hashbrown",
]
[[package]]
@@ -3768,11 +3759,10 @@ dependencies = [
[[package]]
name = "num-bigint-dig"
version = "0.8.4"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
dependencies = [
"byteorder",
"lazy_static",
"libm",
"num-integer",
@@ -4023,6 +4013,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"
@@ -5050,9 +5050,9 @@ dependencies = [
[[package]]
name = "rsa"
version = "0.9.9"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88"
checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
dependencies = [
"const-oid",
"digest",
@@ -5127,15 +5127,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 +5164,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 +5422,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 +5947,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 +6217,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 +6238,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 +6270,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 +6285,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 +6318,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 +6330,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 +6341,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 +6542,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 +7465,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"
+4 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "2.33.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"
+1 -8
View File
@@ -104,14 +104,7 @@ pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
} else {
(name, addr.to_string())
};
let mut name = sanitize_name(name);
// If the 'display name' is just the address, remove it:
// Otherwise, the contact would sometimes be shown as "alice@example.com (alice@example.com)" (see `get_name_n_addr()`).
// If the display name is empty, DC will just show the address when it needs a display name.
if name == addr {
name = "".to_string();
}
let name = sanitize_name(name);
(name, addr)
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "2.33.0"
version = "2.36.0"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"
+136 -60
View File
@@ -22,6 +22,7 @@ typedef struct _dc_lot dc_lot_t;
typedef struct _dc_provider dc_provider_t;
typedef struct _dc_event dc_event_t;
typedef struct _dc_event_emitter dc_event_emitter_t;
typedef struct _dc_event_channel dc_event_channel_t;
typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_t;
typedef struct _dc_backup_provider dc_backup_provider_t;
@@ -429,16 +430,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,
@@ -519,6 +517,10 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `webxdc_realtime_enabled` = Whether the realtime APIs should be enabled.
* 0 = WebXDC realtime API is disabled and behaves as noop.
* 1 = WebXDC realtime API is enabled (default).
* - `who_can_call_me` = Who can cause call notifications.
* 0 = Everybody (except explicitly blocked contacts),
* 1 = Contacts (default, does not include contact requests),
* 2 = Nobody (calls never result in a notification).
*
* If you want to retrieve a value, use dc_get_config().
*
@@ -1611,10 +1613,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
@@ -3092,7 +3094,7 @@ int dc_receive_backup (dc_context_t* context, const char* qr);
/**
* Create a new account manager.
* The account manager takes an directory
* The account manager takes a directory
* where all context-databases are placed in.
* To add a context to the account manager,
* use dc_accounts_add_account() or dc_accounts_migrate_account().
@@ -3114,6 +3116,35 @@ int dc_receive_backup (dc_context_t* context, const char* qr);
*/
dc_accounts_t* dc_accounts_new (const char* dir, int writable);
/**
* Create a new account manager with an existing events channel,
* which allows you to see events emitted during startup.
*
* The account manager takes a directory
* where all context-databases are placed in.
* To add a context to the account manager,
* use dc_accounts_add_account() or dc_accounts_migrate_account().
* All account information are persisted.
* To remove a context from the account manager,
* use dc_accounts_remove_account().
*
* @memberof dc_accounts_t
* @param dir The directory to create the context-databases in.
* If the directory does not exist,
* dc_accounts_new_with_event_channel() will try to create it.
* @param writable Whether the returned account manager is writable, i.e. calling these functions on
* it is possible: dc_accounts_add_account(), dc_accounts_add_closed_account(),
* dc_accounts_migrate_account(), dc_accounts_remove_account(), dc_accounts_select_account().
* @param dc_event_channel_t Events Channel to be used for this accounts manager,
* create one with dc_event_channel_new().
* This channel is consumed by this method and can not be used again afterwards,
* so be sure to call `dc_event_channel_get_event_emitter` before.
* @return An account manager object.
* The object must be passed to the other account manager functions
* and must be freed using dc_accounts_unref() after usage.
* On errors, NULL is returned.
*/
dc_accounts_t* dc_accounts_new_with_event_channel(const char* dir, int writable, dc_event_channel_t* events_channel);
/**
* Free an account manager object.
@@ -3354,8 +3385,12 @@ void dc_accounts_set_push_device_token (dc_accounts_t* accounts, const
* Must be freed using dc_event_emitter_unref() after usage.
*
* Note: Use only one event emitter per account manager.
* Having more than one event emitter running at the same time on the same account manager
* will result in events randomly delivered to the one or to the other.
* The result of having multiple event emitters is unspecified.
* Currently events are broadcasted to all existing event emitters,
* but previous versions delivered events to only one event emitter
* and this behavior may change again in the future.
* Events emitted before creation of event emitter
* are not available to event emitter.
*/
dc_event_emitter_t* dc_accounts_get_event_emitter (dc_accounts_t* accounts);
@@ -5080,7 +5115,6 @@ void dc_msg_force_plaintext (dc_msg_t* msg);
* By default, these names are equal,
* but functions working with contact names
* (e.g. dc_contact_get_name(), dc_contact_get_display_name(),
* dc_contact_get_name_n_addr(),
* dc_create_contact() or dc_add_address_book())
* only affect the given-name.
*/
@@ -5130,7 +5164,7 @@ char* dc_contact_get_addr (const dc_contact_t* contact);
* The function does not return the contact name as received from the network.
*
* This name is typically used in a form where the user can edit the name of a contact.
* To get a fine name to display in lists etc., use dc_contact_get_display_name() or dc_contact_get_name_n_addr().
* To get a fine name to display in lists etc., use dc_contact_get_display_name().
*
* @memberof dc_contact_t
* @param contact The contact object.
@@ -5185,23 +5219,6 @@ char* dc_contact_get_display_name (const dc_contact_t* contact);
#define dc_contact_get_first_name dc_contact_get_display_name
/**
* Get a summary of name and address.
*
* The returned string is either "Name (email@domain.com)" or just
* "email@domain.com" if the name is unset.
*
* The summary is typically used when asking the user something about the contact.
* The attached e-mail address makes the question unique, e.g. "Chat with Alan Miller (am@uniquedomain.com)?"
*
* @memberof dc_contact_t
* @param contact The contact object.
* @return A summary string, must be released using dc_str_unref().
* Never returns NULL.
*/
char* dc_contact_get_name_n_addr (const dc_contact_t* contact);
/**
* Get the contact's profile image.
* This is the image set by each remote user on their own
@@ -5328,8 +5345,8 @@ int dc_contact_is_key_contact (dc_contact_t* contact);
*
* - If dc_contact_get_verifier_id() != 0,
* display text "Introduced by ..."
* with the name and address of the contact
* formatted by dc_contact_get_name_n_addr().
* with the name of the contact
* formatted by dc_contact_get_name().
* Prefix the text by a green checkmark.
*
* - If dc_contact_get_verifier_id() == 0 and dc_contact_is_verified() != 0,
@@ -5999,6 +6016,62 @@ char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);
*/
char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *input);
/**
* @class dc_event_channel_t
*
* Opaque object that is used to create an event emitter which can be used log events during startup of an accounts manger.
* Only used for dc_accounts_new_with_event_channel().
* To use it:
* 1. create an events channel with `dc_event_channel_new()`.
* 2. get an event emitter for it with `dc_event_channel_get_event_emitter()`.
* 3. use it to create your account manager with `dc_accounts_new_with_event_channel()`, which consumes the channel.
* 4. free the empty channel wrapper object with `dc_event_channel_unref()`.
*/
/**
* Create a new event channel.
*
* @memberof dc_event_channel_t
* @return An event channel wrapper object (dc_event_channel_t).
*/
dc_event_channel_t* dc_event_channel_new();
/**
* Release/free the events channel structure.
* This function releases the memory of the `dc_event_channel_t` structure.
*
* you can call it after calling dc_accounts_new_with_event_channel,
* which took the events channel out of it already, so this just frees the underlying option.
*
* @memberof dc_event_channel_t
*/
void dc_event_channel_unref(dc_event_channel_t* event_channel);
/**
* Create the event emitter that is used to receive events.
*
* The library will emit various @ref DC_EVENT events, such as "new message", "message read" etc.
* To get these events, you have to create an event emitter using this function
* and call dc_get_next_event() on the emitter.
*
* This is similar to dc_get_event_emitter(), which, however,
* must not be called for accounts handled by the account manager.
*
* @memberof dc_event_channel_t
* @param The event channel.
* @return Returns the event emitter, NULL on errors.
* Must be freed using dc_event_emitter_unref() after usage.
*
* Note: Use only one event emitter per account manager / event channel.
* The result of having multiple event emitters is unspecified.
* Currently events are broadcasted to all existing event emitters,
* but previous versions delivered events to only one event emitter
* and this behavior may change again in the future.
* Events emitted before creation of event emitter
* are not available to event emitter.
*/
dc_event_emitter_t* dc_event_channel_get_event_emitter(dc_event_channel_t* event_channel);
/**
* @class dc_event_emitter_t
*
@@ -6702,6 +6775,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 +7387,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
@@ -7372,8 +7449,7 @@ void dc_event_unref(dc_event_t* event);
/// May be followed by the info-messages
/// #DC_STR_SECURE_JOIN_REPLIES, #DC_STR_CONTACT_VERIFIED and #DC_STR_MSGADDMEMBER.
///
/// `%1$s` will be replaced by name and address of the inviter,
/// `%2$s` will be replaced by the name of the inviter.
/// `%1$s` and `%2$s` will be replaced by name of the inviter.
#define DC_STR_SECURE_JOIN_STARTED 117
/// "%1$s replied, waiting for being added to the group…"
@@ -7390,7 +7466,7 @@ void dc_event_unref(dc_event_t* event);
///
/// Subtitle for verification qrcode svg image generated by the core.
///
/// `%1$s` will be replaced by name and address of the inviter.
/// `%1$s` will be replaced by name of the inviter.
#define DC_STR_SETUP_CONTACT_QR_DESC 119
/// "Scan to join %1$s"
@@ -7421,7 +7497,7 @@ void dc_event_unref(dc_event_t* event);
///
/// `%1$s` will be replaced by the old group name.
/// `%2$s` will be replaced by the new group name.
/// `%3$s` will be replaced by name and address of the contact who did the action.
/// `%3$s` will be replaced by name of the contact who did the action.
#define DC_STR_GROUP_NAME_CHANGED_BY_OTHER 125
/// "You changed the group image."
@@ -7429,7 +7505,7 @@ void dc_event_unref(dc_event_t* event);
/// "Group image changed by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact who did the action.
/// `%1$s` will be replaced by name of the contact who did the action.
#define DC_STR_GROUP_IMAGE_CHANGED_BY_OTHER 127
/// "You added member %1$s."
@@ -7441,23 +7517,23 @@ void dc_event_unref(dc_event_t* event);
/// "Member %1$s added by %2$s."
///
/// `%1$s` will be replaced by name and address of the contact added to the group.
/// `%2$s` will be replaced by name and address of the contact who did the action.
/// `%1$s` will be replaced by name of the contact added to the group.
/// `%2$s` will be replaced by name of the contact who did the action.
///
/// Used in status messages.
#define DC_STR_ADD_MEMBER_BY_OTHER 129
/// "You removed member %1$s."
///
/// `%1$s` will be replaced by name and address of the contact removed from the group.
/// `%1$s` will be replaced by name of the contact removed from the group.
///
/// Used in status messages.
#define DC_STR_REMOVE_MEMBER_BY_YOU 130
/// "Member %1$s removed by %2$s."
///
/// `%1$s` will be replaced by name and address of the contact removed from the group.
/// `%2$s` will be replaced by name and address of the contact who did the action.
/// `%1$s` will be replaced by name of the contact removed from the group.
/// `%2$s` will be replaced by name of the contact who did the action.
///
/// Used in status messages.
#define DC_STR_REMOVE_MEMBER_BY_OTHER 131
@@ -7469,7 +7545,7 @@ void dc_event_unref(dc_event_t* event);
/// "Group left by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
/// `%1$s` will be replaced by name of the contact.
///
/// Used in status messages.
#define DC_STR_GROUP_LEFT_BY_OTHER 133
@@ -7481,7 +7557,7 @@ void dc_event_unref(dc_event_t* event);
/// "Group image deleted by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
/// `%1$s` will be replaced by name of the contact.
///
/// Used in status messages.
#define DC_STR_GROUP_IMAGE_DELETED_BY_OTHER 135
@@ -7493,7 +7569,7 @@ void dc_event_unref(dc_event_t* event);
/// "Location streaming enabled by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
/// `%1$s` will be replaced by name of the contact.
///
/// Used in status messages.
#define DC_STR_LOCATION_ENABLED_BY_OTHER 137
@@ -7505,7 +7581,7 @@ void dc_event_unref(dc_event_t* event);
/// "Message deletion timer is disabled by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
/// `%1$s` will be replaced by name of the contact.
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_DISABLED_BY_OTHER 139
@@ -7520,7 +7596,7 @@ void dc_event_unref(dc_event_t* event);
/// "Message deletion timer is set to %1$s s by %2$s."
///
/// `%1$s` will be replaced by the number of seconds (always >1) the timer is set to.
/// `%2$s` will be replaced by name and address of the contact.
/// `%2$s` will be replaced by name of the contact.
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_SECONDS_BY_OTHER 141
@@ -7532,7 +7608,7 @@ void dc_event_unref(dc_event_t* event);
/// "Message deletion timer is set to 1 minute by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
/// `%1$s` will be replaced by name of the contact.
/// @deprecated 2025-11-14, this string is no longer needed
#define DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_OTHER 143
@@ -7543,7 +7619,7 @@ void dc_event_unref(dc_event_t* event);
/// "Message deletion timer is set to 1 hour by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
/// `%1$s` will be replaced by name of the contact.
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_OTHER 145
@@ -7555,7 +7631,7 @@ void dc_event_unref(dc_event_t* event);
/// "Message deletion timer is set to 1 day by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
/// `%1$s` will be replaced by name of the contact.
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_1_DAY_BY_OTHER 147
@@ -7567,7 +7643,7 @@ void dc_event_unref(dc_event_t* event);
/// "Message deletion timer is set to 1 week by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
/// `%1$s` will be replaced by name of the contact.
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_OTHER 149
@@ -7584,7 +7660,7 @@ void dc_event_unref(dc_event_t* event);
/// Used in status messages.
///
/// `%1$s` will be replaced by the number of minutes (always >1) the timer is set to.
/// `%2$s` will be replaced by name and address of the contact.
/// `%2$s` will be replaced by name of the contact.
#define DC_STR_EPHEMERAL_TIMER_MINUTES_BY_OTHER 151
/// "You set message deletion timer to %1$s hours."
@@ -7599,7 +7675,7 @@ void dc_event_unref(dc_event_t* event);
/// Used in status messages.
///
/// `%1$s` will be replaced by the number of hours (always >1) the timer is set to.
/// `%2$s` will be replaced by name and address of the contact.
/// `%2$s` will be replaced by name of the contact.
#define DC_STR_EPHEMERAL_TIMER_HOURS_BY_OTHER 153
/// "You set message deletion timer to %1$s days."
@@ -7614,7 +7690,7 @@ void dc_event_unref(dc_event_t* event);
/// Used in status messages.
///
/// `%1$s` will be replaced by the number of days (always >1) the timer is set to.
/// `%2$s` will be replaced by name and address of the contact.
/// `%2$s` will be replaced by name of the contact.
#define DC_STR_EPHEMERAL_TIMER_DAYS_BY_OTHER 155
/// "You set message deletion timer to %1$s weeks."
@@ -7629,7 +7705,7 @@ void dc_event_unref(dc_event_t* event);
/// Used in status messages.
///
/// `%1$s` will be replaced by the number of weeks (always >1) the timer is set to.
/// `%2$s` will be replaced by name and address of the contact.
/// `%2$s` will be replaced by name of the contact.
#define DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER 157
/// "You set message deletion timer to 1 year."
@@ -7639,14 +7715,14 @@ void dc_event_unref(dc_event_t* event);
/// "Message deletion timer is set to 1 year by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
/// `%1$s` will be replaced by name of the contact.
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_1_YEAR_BY_OTHER 159
/// "Scan to set up second device for %1$s"
///
/// `%1$s` will be replaced by name and address of the account.
/// `%1$s` will be replaced by name of the account.
#define DC_STR_BACKUP_TRANSFER_QR 162
/// "Account transferred to your second device."
+122 -63
View File
@@ -15,10 +15,9 @@ 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};
use std::sync::{Arc, LazyLock, Mutex};
use std::time::{Duration, SystemTime};
use anyhow::Context as _;
@@ -4203,18 +4202,6 @@ pub unsafe extern "C" fn dc_contact_get_display_name(
ffi_contact.contact.get_display_name().strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_contact_get_name_n_addr(
contact: *mut dc_contact_t,
) -> *mut libc::c_char {
if contact.is_null() {
eprintln!("ignoring careless call to dc_contact_get_name_n_addr()");
return "".strdup();
}
let ffi_contact = &*contact;
ffi_contact.contact.get_name_n_addr().strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_contact_get_profile_image(
contact: *mut dc_contact_t,
@@ -4739,33 +4726,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() {
@@ -4776,7 +4743,99 @@ 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:#}");
ptr::null_mut()
}
}
}
pub type dc_event_channel_t = Mutex<Option<Events>>;
#[no_mangle]
pub unsafe extern "C" fn dc_event_channel_new() -> *mut dc_event_channel_t {
Box::into_raw(Box::new(Mutex::new(Some(Events::new()))))
}
/// Release the events channel structure.
///
/// This function releases the memory of the `dc_event_channel_t` structure.
///
/// you can call it after calling dc_accounts_new_with_event_channel,
/// which took the events channel out of it already, so this just frees the underlying option.
#[no_mangle]
pub unsafe extern "C" fn dc_event_channel_unref(event_channel: *mut dc_event_channel_t) {
if event_channel.is_null() {
eprintln!("ignoring careless call to dc_event_channel_unref()");
return;
}
drop(Box::from_raw(event_channel))
}
#[no_mangle]
pub unsafe extern "C" fn dc_event_channel_get_event_emitter(
event_channel: *mut dc_event_channel_t,
) -> *mut dc_event_emitter_t {
if event_channel.is_null() {
eprintln!("ignoring careless call to dc_event_channel_get_event_emitter()");
return ptr::null_mut();
}
let Some(event_channel) = &*(*event_channel)
.lock()
.expect("call to dc_event_channel_get_event_emitter() failed: mutex is poisoned")
else {
eprintln!(
"ignoring careless call to dc_event_channel_get_event_emitter()
-> channel was already consumed, make sure you call this before dc_accounts_new_with_event_channel"
);
return ptr::null_mut();
};
let emitter = event_channel.get_emitter();
Box::into_raw(Box::new(emitter))
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_new_with_event_channel(
dir: *const libc::c_char,
writable: libc::c_int,
event_channel: *mut dc_event_channel_t,
) -> *const dc_accounts_t {
setup_panic!();
if dir.is_null() || event_channel.is_null() {
eprintln!("ignoring careless call to dc_accounts_new_with_event_channel()");
return ptr::null_mut();
}
// consuming channel enforce that you need to get the event emitter
// before initializing the account manager,
// so that you don't miss events/errors during initialisation.
// It also prevents you from using the same channel on multiple account managers.
let Some(event_channel) = (*event_channel)
.lock()
.expect("call to dc_event_channel_get_event_emitter() failed: mutex is poisoned")
.take()
else {
eprintln!(
"ignoring careless call to dc_accounts_new_with_event_channel()
-> channel was already consumed"
);
return ptr::null_mut();
};
let accs = block_on(Accounts::new_with_events(
as_path(dir).into(),
writable != 0,
event_channel,
));
match 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:#}");
@@ -4789,17 +4848,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);
drop(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() {
@@ -4816,7 +4875,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()");
@@ -4832,7 +4891,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() {
@@ -4856,13 +4915,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;
@@ -4877,13 +4936,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;
@@ -4899,7 +4958,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() {
@@ -4907,7 +4966,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;
@@ -4925,7 +4984,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() {
@@ -4933,7 +4992,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 {
@@ -4954,7 +5013,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();
@@ -4968,18 +5027,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;
@@ -4990,7 +5049,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;
@@ -5001,7 +5060,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;
@@ -5013,7 +5072,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 {
@@ -5032,7 +5091,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;
@@ -5044,7 +5103,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() {
@@ -5067,7 +5126,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()");
@@ -5087,16 +5146,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();
+1 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "2.33.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]
+11 -12
View File
@@ -15,7 +15,7 @@ use deltachat::chat::{
MessageListOptions,
};
use deltachat::chatlist::Chatlist;
use deltachat::config::Config;
use deltachat::config::{get_all_ui_config_keys, Config};
use deltachat::constants::DC_MSG_ID_DAYMARKER;
use deltachat::contact::{may_be_valid_addr, Contact, ContactId, Origin};
use deltachat::context::get_info;
@@ -35,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;
@@ -330,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)
}
@@ -459,6 +452,12 @@ impl CommandApi {
Ok(result)
}
/// Returns all `ui.*` config keys that were set by the UI.
async fn get_all_ui_config_keys(&self, account_id: u32) -> Result<Vec<String>> {
let ctx = self.get_context(account_id).await?;
get_all_ui_config_keys(&ctx).await
}
async fn set_stock_strings(&self, strings: HashMap<u32, String>) -> Result<()> {
let accounts = self.accounts.read().await;
for (stock_id, stock_message) in strings {
@@ -802,11 +801,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
+1 -4
View File
@@ -17,7 +17,6 @@ pub struct ContactObject {
id: u32,
name: String,
profile_image: Option<String>, // BLOBS
name_and_addr: String,
is_blocked: bool,
/// Is the contact a key contact.
@@ -47,8 +46,7 @@ pub struct ContactObject {
///
/// - If `verifierId` != 0,
/// display text "Introduced by ..."
/// with the name and address of the contact
/// formatted by `name_and_addr`/`nameAndAddr`.
/// with the name of the contact.
/// Prefix the text by a green checkmark.
///
/// - If `verifierId` == 0 and `isVerified` != 0,
@@ -97,7 +95,6 @@ impl ContactObject {
id: contact.id.to_u32(),
name: contact.get_name().to_owned(),
profile_image, //BLOBS
name_and_addr: contact.get_name_n_addr(),
is_blocked: contact.is_blocked(),
is_key_contact: contact.is_key_contact(),
e2ee_avail: contact.e2ee_avail(context).await?,
+5 -5
View File
@@ -463,11 +463,11 @@ pub enum EventType {
/// One or more transports has changed.
///
/// This event is used for tests to detect when transport
/// synchronization messages arrives.
/// UIs don't need to use it, it is unlikely
/// that user modifies transports on multiple
/// devices simultaneously.
/// 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,
}
+1 -1
View File
@@ -54,5 +54,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "2.33.0"
"version": "2.36.0"
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "2.33.0"
version = "2.36.0"
license = "MPL-2.0"
edition = "2021"
repository = "https://github.com/chatmail/core"
+6 -4
View File
@@ -1158,15 +1158,17 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let contact_id = ContactId::new(arg1.parse()?);
let contact = Contact::get_by_id(&context, contact_id).await?;
let name_n_addr = contact.get_name_n_addr();
let display_name = contact.get_display_name();
let addr = contact.get_addr();
let mut res = format!(
"Contact info for: {}:\nIcon: {}\n",
name_n_addr,
"Contact info for: {}:\nIcon: {}\nAddr: {}\n",
display_name,
match contact.get_profile_image(&context).await? {
Some(image) => image.to_str().unwrap().to_string(),
None => "NoIcon".to_string(),
}
},
addr
);
res += &Contact::get_encrinfo(&context, contact_id).await?;
+5 -5
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.");
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "2.33.0"
version = "2.36.0"
license = "MPL-2.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
+45
View File
@@ -107,3 +107,48 @@ def test_no_contact_request_call(acfactory) -> None:
msg = bob.get_message_by_id(event.msg_id)
if msg.get_snapshot().text == "Hello!":
break
def test_who_can_call_me_nobody(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
# Bob sets "who can call me" to "nobody" (2)
bob.set_config("who_can_call_me", "2")
# Bob even accepts Alice in advance so the chat does not appear as contact request.
bob.create_chat(alice)
alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.place_outgoing_call("offer")
alice_chat_bob.send_text("Hello!")
# Notification for "Hello!" message should arrive
# without the call ringing.
while True:
event = bob.wait_for_event()
# There should be no incoming call notification.
assert event.kind != EventType.INCOMING_CALL
if event.kind == EventType.INCOMING_MSG:
msg = bob.get_message_by_id(event.msg_id)
if msg.get_snapshot().text == "Hello!":
break
def test_who_can_call_me_everybody(acfactory) -> None:
"""Test that if "who can call me" setting is set to "everybody", calls arrive even in contact request chats."""
alice, bob = acfactory.get_online_accounts(2)
# Bob sets "who can call me" to "nobody" (0)
bob.set_config("who_can_call_me", "0")
alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.place_outgoing_call("offer")
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
incoming_call_message = Message(bob, incoming_call_event.msg_id)
# Even with the call arriving, the chat is still in the contact request mode.
incoming_chat = incoming_call_message.get_snapshot().chat
assert incoming_chat.get_basic_snapshot().is_contact_request
@@ -42,3 +42,16 @@ def test_send_and_receive_message(alice_and_remote_bob) -> None:
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')")
@@ -188,6 +188,10 @@ def test_transport_synchronization(acfactory, log) -> None:
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"])
@@ -217,3 +221,58 @@ def test_recognize_self_address(acfactory) -> None:
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())
@@ -696,6 +696,6 @@ def test_withdraw_securejoin_qr(acfactory):
event = alice.wait_for_event()
if (
event.kind == EventType.WARNING
and "Ignoring vg-request-with-auth message because of invalid auth code." in event.msg
and "Ignoring RequestWithAuth message because of invalid auth code." in event.msg
):
break
+104 -8
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):
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "2.33.0"
version = "2.36.0"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"
@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "2.33.0"
"version": "2.36.0"
}
+1 -1
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() {
+7 -3
View File
@@ -16,7 +16,12 @@ ignore = [
# 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"
"RUSTSEC-2025-0134",
# Old versions of "lru" are transitive dependencies of iroh 0.35.0.
# <https://rustsec.org/advisories/RUSTSEC-2026-0002>
# <https://github.com/chatmail/core/issues/7692>
"RUSTSEC-2026-0002",
]
[bans]
@@ -31,11 +36,10 @@ skip = [
{ name = "derive_more", version = "1.0.0" },
{ name = "event-listener", version = "2.5.3" },
{ name = "getrandom", version = "0.2.12" },
{ name = "hashbrown", version = "0.14.5" },
{ name = "heck", version = "0.4.1" },
{ name = "http", version = "0.2.12" },
{ name = "linux-raw-sys", version = "0.4.14" },
{ name = "lru", version = "0.12.3" },
{ name = "lru", version = "0.12.5" },
{ name = "netlink-packet-route", version = "0.17.1" },
{ name = "nom", version = "7.1.3" },
{ name = "rand_chacha", version = "0.3.1" },
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "2.33.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"
+10 -89
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):
+1 -1
View File
@@ -1 +1 @@
2025-12-05
2026-01-03
+1 -1
View File
@@ -7,7 +7,7 @@ set -euo pipefail
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.91.0
RUST_VERSION=1.92.0
ARCH="$(uname -m)"
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
+13 -3
View File
@@ -60,8 +60,18 @@ impl Accounts {
if writable && !dir.exists() {
Accounts::create(&dir).await?;
}
let events = Events::new();
Accounts::open(events, dir, writable).await
}
Accounts::open(dir, writable).await
/// Loads or creates an accounts folder at the given `dir`.
/// Uses an existing events channel.
pub async fn new_with_events(dir: PathBuf, writable: bool, events: Events) -> Result<Self> {
if writable && !dir.exists() {
Accounts::create(&dir).await?;
}
Accounts::open(events, dir, writable).await
}
/// Get the ID used to log events.
@@ -85,14 +95,14 @@ impl Accounts {
/// Opens an existing accounts structure. Will error if the folder doesn't exist,
/// no account exists and no config exists.
async fn open(dir: PathBuf, writable: bool) -> Result<Self> {
async fn open(events: Events, dir: PathBuf, writable: bool) -> Result<Self> {
ensure!(dir.exists(), "directory does not exist");
let config_file = dir.join(CONFIG_NAME);
ensure!(config_file.exists(), "{config_file:?} does not exist");
let config = Config::from_file(config_file, writable).await?;
let events = Events::new();
let stockstrings = StockStrings::new();
let push_subscriber = PushSubscriber::new();
let accounts = config
+75 -26
View File
@@ -4,6 +4,7 @@
//! This means, the "Call ID" is a "Message ID" - similar to Webxdc IDs.
use crate::chat::ChatIdBlocked;
use crate::chat::{Chat, ChatId, send_msg};
use crate::config::Config;
use crate::constants::{Blocked, Chattype};
use crate::contact::ContactId;
use crate::context::{Context, WeakContext};
@@ -16,6 +17,8 @@ use crate::net::dns::lookup_host_with_cache;
use crate::param::Param;
use crate::tools::{normalize_text, time};
use anyhow::{Context as _, Result, ensure};
use deltachat_derive::{FromSql, ToSql};
use num_traits::FromPrimitive;
use sdp::SessionDescription;
use serde::Serialize;
use std::io::Cursor;
@@ -33,7 +36,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*
@@ -348,26 +351,34 @@ impl Context {
false
}
};
if let Some(chat_id_blocked) =
ChatIdBlocked::lookup_by_contact(self, from_id).await?
{
match chat_id_blocked.blocked {
Blocked::Not => {
self.emit_event(EventType::IncomingCall {
msg_id: call.msg.id,
chat_id: call.msg.chat_id,
place_call_info: call.place_call_info.to_string(),
has_video,
});
}
Blocked::Yes | Blocked::Request => {
// Do not notify about incoming calls
// from contact requests and blocked contacts.
//
// User can still access the call and accept it
// via the chat in case of contact requests.
}
}
let can_call_me = match who_can_call_me(self).await? {
WhoCanCallMe::Contacts => ChatIdBlocked::lookup_by_contact(self, from_id)
.await?
.is_some_and(|chat_id_blocked| {
match chat_id_blocked.blocked {
Blocked::Not => true,
Blocked::Yes | Blocked::Request => {
// Do not notify about incoming calls
// from contact requests and blocked contacts.
//
// User can still access the call and accept it
// via the chat in case of contact requests.
false
}
}
}),
WhoCanCallMe::Everybody => ChatIdBlocked::lookup_by_contact(self, from_id)
.await?
.is_none_or(|chat_id_blocked| chat_id_blocked.blocked != Blocked::Yes),
WhoCanCallMe::Nobody => false,
};
if can_call_me {
self.emit_event(EventType::IncomingCall {
msg_id: call.msg.id,
chat_id: call.msg.chat_id,
place_call_info: call.place_call_info.to_string(),
has_video,
});
}
let wait = call.remaining_ring_seconds();
let context = self.get_weak_context();
@@ -663,9 +674,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)
@@ -673,14 +682,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)
}
@@ -701,5 +723,32 @@ pub async fn ice_servers(context: &Context) -> Result<String> {
}
}
/// "Who can call me" config options.
#[derive(
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
)]
#[repr(u8)]
pub enum WhoCanCallMe {
/// Everybody can call me if they are not blocked.
///
/// This includes contact requests.
Everybody = 0,
/// Every contact who is not blocked and not a contact request, can call.
#[default]
Contacts = 1,
/// Nobody can call me.
Nobody = 2,
}
/// Returns currently configuration of the "who can call me" option.
async fn who_can_call_me(context: &Context) -> Result<WhoCanCallMe> {
let who_can_call_me =
WhoCanCallMe::from_i32(context.get_config_int(Config::WhoCanCallMe).await?)
.unwrap_or_default();
Ok(who_can_call_me)
}
#[cfg(test)]
mod calls_tests;
+33 -28
View File
@@ -432,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?;
}
}
}
@@ -596,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
}
@@ -654,7 +662,7 @@ impl ChatId {
context
.set_config_internal(Config::LastHousekeeping, None)
.await?;
context.scheduler.interrupt_inbox().await;
context.scheduler.interrupt_smtp().await;
Ok(())
}
@@ -2107,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(())
}
@@ -2728,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
@@ -2852,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)
};
@@ -3090,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 == ?
);",
@@ -3837,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));
@@ -4311,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(())
}
+29 -4
View File
@@ -396,8 +396,6 @@ impl Chatlist {
if lastmsg.from_id == ContactId::SELF {
None
} else if chat.typ == Chattype::Group
|| chat.typ == Chattype::OutBroadcast
|| chat.typ == Chattype::InBroadcast
|| chat.typ == Chattype::Mailinglist
|| chat.is_self_talk()
{
@@ -471,10 +469,11 @@ mod tests {
use super::*;
use crate::chat::save_msgs;
use crate::chat::{
add_contact_to_chat, create_group, get_chat_contacts, remove_contact_from_chat,
send_text_msg, set_chat_name,
add_contact_to_chat, create_broadcast, create_group, get_chat_contacts,
remove_contact_from_chat, send_text_msg, set_chat_name,
};
use crate::receive_imf::receive_imf;
use crate::securejoin::get_securejoin_qr;
use crate::stock_str::StockMessage;
use crate::test_utils::TestContext;
use crate::test_utils::TestContextManager;
@@ -806,6 +805,32 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_no_summary_prefix_for_channel() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let alice_chat_id = create_broadcast(&alice, "alice's channel".to_string()).await?;
let qr = get_securejoin_qr(&alice, Some(alice_chat_id)).await?;
tcm.exec_securejoin_qr(&bob, &alice, &qr).await;
send_text_msg(&alice, alice_chat_id, "hi".into()).await?;
let sent1 = alice.pop_sent_msg().await;
let chatlist = Chatlist::try_load(&alice, 0, None, None).await?;
let summary = chatlist.get_summary(&alice, 0, None).await?;
assert!(summary.prefix.is_none());
assert_eq!(summary.text, "hi");
bob.recv_msg(&sent1).await;
let chatlist = Chatlist::try_load(&bob, 0, None, None).await?;
let summary = chatlist.get_summary(&bob, 0, None).await?;
assert!(summary.prefix.is_none());
assert_eq!(summary.text, "hi");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_load_broken() {
let t = TestContext::new_bob().await;
+59 -40
View File
@@ -20,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, add_pseudo_transport};
use crate::transport::{ConfiguredLoginParam, add_pseudo_transport, send_sync_transports};
use crate::{constants, stats};
/// The available configuration keys.
@@ -446,6 +446,16 @@ pub enum Config {
/// Protected Email".
#[strum(props(default = "1"))]
StdHeaderProtectionComposing,
/// Who can call me.
///
/// The options are from the `WhoCanCallMe` enum.
#[strum(props(default = "1"))]
WhoCanCallMe,
/// Experimental option denoting that the current profile is shared between multiple team members.
/// For now, the only effect of this option is that seen flags are not synchronized.
TeamProfile,
}
impl Config {
@@ -504,7 +514,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?,
@@ -605,12 +615,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? {
@@ -818,37 +822,39 @@ impl Context {
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?;
@@ -878,7 +884,7 @@ impl Context {
{
return Ok(());
}
self.scheduler.interrupt_inbox().await;
self.scheduler.interrupt_smtp().await;
Ok(())
}
@@ -942,7 +948,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();
self.quota.write().await.clear();
self.sql
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(primary_new))
@@ -987,5 +993,18 @@ fn get_config_keys_string() -> String {
format!(" {keys} ")
}
/// Returns all `ui.*` config keys that were set by the UI.
pub async fn get_all_ui_config_keys(context: &Context) -> Result<Vec<String>> {
let ui_keys = context
.sql
.query_map_vec(
"SELECT keyname FROM config WHERE keyname GLOB 'ui.*' ORDER BY config.id",
(),
|row| Ok(row.get::<_, String>(0)?),
)
.await?;
Ok(ui_keys)
}
#[cfg(test)]
mod config_tests;
+33 -2
View File
@@ -81,6 +81,37 @@ async fn test_ui_config() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_all_ui_config_keys() -> Result<()> {
let t = TestContext::new().await;
t.set_ui_config("ui.android.screen_security", Some("safe"))
.await?;
t.set_ui_config("ui.lastchatid", Some("231")).await?;
t.set_ui_config(
"ui.desktop.webxdcBounds.528490",
Some(r#"{"x":954,"y":356,"width":378,"height":671}"#),
)
.await?;
t.set_ui_config(
"ui.desktop.webxdcBounds.556543",
Some(r#"{"x":954,"y":356,"width":378,"height":671}"#),
)
.await?;
assert_eq!(
get_all_ui_config_keys(&t).await?,
vec![
"ui.android.screen_security",
"ui.lastchatid",
"ui.desktop.webxdcBounds.528490",
"ui.desktop.webxdcBounds.556543"
]
);
Ok(())
}
/// Regression test for https://github.com/deltachat/deltachat-core-rust/issues/3012
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_config_bool() -> Result<()> {
@@ -237,7 +268,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;
@@ -261,7 +292,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?;
+49 -8
View File
@@ -45,6 +45,10 @@ use crate::transport::{
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!(
@@ -206,7 +210,8 @@ impl Context {
/// (i.e. [EnteredLoginParam::addr]).
pub async fn delete_transport(&self, addr: &str) -> Result<()> {
let now = time();
self.sql
let removed_transport_id = self
.sql
.transaction(|transaction| {
let primary_addr = transaction.query_row(
"SELECT value FROM config WHERE keyname='configured_addr'",
@@ -247,10 +252,11 @@ impl Context {
(addr, remove_timestamp),
)?;
Ok(())
Ok(transport_id)
})
.await?;
send_sync_transports(self).await?;
self.quota.write().await.remove(&removed_transport_id);
Ok(())
}
@@ -268,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?;
+1 -4
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'.');
+12 -27
View File
@@ -434,7 +434,7 @@ pub struct Contact {
pub id: ContactId,
/// Contact name. It is recommended to use `Contact::get_name`,
/// `Contact::get_display_name` or `Contact::get_name_n_addr` to access this field.
/// `Contact::get_display_name` to access this field.
/// May be empty, initially set to `authname`.
name: String,
@@ -1144,7 +1144,7 @@ WHERE c.id>?
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.last_seen DESC, c.id DESC
ORDER BY c.origin>=? DESC, c.last_seen DESC, c.id DESC
",
(
ContactId::LAST_SPECIAL,
@@ -1152,6 +1152,7 @@ ORDER BY c.last_seen DESC, c.id DESC
minimal_origin,
&s3str_like_cmd,
&s3str_like_cmd,
Origin::CreateChat,
),
|row| {
let id: ContactId = row.get(0)?;
@@ -1201,8 +1202,13 @@ ORDER BY c.last_seen DESC, c.id DESC
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)?;
@@ -1518,7 +1524,7 @@ WHERE addr=?
/// May be an empty string.
///
/// This name is typically used in a form where the user can edit the name of a contact.
/// To get a fine name to display in lists etc., use `Contact::get_display_name` or `Contact::get_name_n_addr`.
/// To get a fine name to display in lists etc., use `Contact::get_display_name`.
pub fn get_name(&self) -> &str {
&self.name
}
@@ -1538,26 +1544,6 @@ WHERE addr=?
&self.addr
}
/// Get a summary of name and address.
///
/// The returned string is either "Name (email@domain.com)" or just
/// "email@domain.com" if the name is unset.
///
/// The result should only be used locally and never sent over the network
/// as it leaks the local contact name.
///
/// The summary is typically used when asking the user something about the contact.
/// The attached email address makes the question unique, eg. "Chat with Alan Miller (am@uniquedomain.com)?"
pub fn get_name_n_addr(&self) -> String {
if !self.name.is_empty() {
format!("{} ({})", self.name, self.addr)
} else if !self.authname.is_empty() {
format!("{} ({})", self.authname, self.addr)
} else {
(&self.addr).into()
}
}
/// Get the contact's profile image.
/// This is the image set by each remote user on their own
/// using set_config(context, "selfavatar", image).
@@ -1652,8 +1638,7 @@ WHERE addr=?
///
/// If this returns Some(_),
/// display green checkmark in the profile and "Introduced by ..." line
/// with the name and address of the contact
/// formatted by [Self::get_name_n_addr].
/// with the name of the contact.
///
/// If this returns `Some(None)`, then the contact is verified,
/// but it's unclear by whom.
-9
View File
@@ -192,7 +192,6 @@ async fn test_add_or_lookup() {
assert_eq!(contact.get_authname(), "bla foo");
assert_eq!(contact.get_display_name(), "Name one");
assert_eq!(contact.get_addr(), "one@eins.org");
assert_eq!(contact.get_name_n_addr(), "Name one (one@eins.org)");
// modify first added contact
let (contact_id_test, sth_modified) = Contact::add_or_lookup(
@@ -225,7 +224,6 @@ async fn test_add_or_lookup() {
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "three@drei.sam");
assert_eq!(contact.get_addr(), "three@drei.sam");
assert_eq!(contact.get_name_n_addr(), "three@drei.sam");
// add name to third contact from incoming message (this becomes authorized name)
let (contact_id_test, sth_modified) = Contact::add_or_lookup(
@@ -239,7 +237,6 @@ async fn test_add_or_lookup() {
assert_eq!(contact_id, contact_id_test);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_name_n_addr(), "m. serious (three@drei.sam)");
assert!(!contact.is_blocked());
// manually edit name of third contact (does not changed authorized name)
@@ -255,7 +252,6 @@ async fn test_add_or_lookup() {
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "m. serious");
assert_eq!(contact.get_name_n_addr(), "schnucki (three@drei.sam)");
assert!(!contact.is_blocked());
// Fourth contact:
@@ -273,7 +269,6 @@ async fn test_add_or_lookup() {
assert_eq!(contact.get_name(), "Wonderland, Alice");
assert_eq!(contact.get_display_name(), "Wonderland, Alice");
assert_eq!(contact.get_addr(), "alice@w.de");
assert_eq!(contact.get_name_n_addr(), "Wonderland, Alice (alice@w.de)");
// check SELF
let contact = Contact::get_by_id(&t, ContactId::SELF).await.unwrap();
@@ -311,7 +306,6 @@ async fn test_contact_name_changes() -> Result<()> {
assert_eq!(contact.get_authname(), "");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "f@example.org");
assert_eq!(contact.get_name_n_addr(), "f@example.org");
let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?;
assert_eq!(contacts.len(), 0);
@@ -337,7 +331,6 @@ async fn test_contact_name_changes() -> Result<()> {
assert_eq!(contact.get_authname(), "Flobbyfoo");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "Flobbyfoo");
assert_eq!(contact.get_name_n_addr(), "Flobbyfoo (f@example.org)");
let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?;
assert_eq!(contacts.len(), 0);
let contacts = Contact::get_all(&t, 0, Some("flobbyfoo")).await?;
@@ -367,7 +360,6 @@ async fn test_contact_name_changes() -> Result<()> {
assert_eq!(contact.get_authname(), "Foo Flobby");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "Foo Flobby");
assert_eq!(contact.get_name_n_addr(), "Foo Flobby (f@example.org)");
let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?;
assert_eq!(contacts.len(), 0);
let contacts = Contact::get_all(&t, 0, Some("flobbyfoo")).await?;
@@ -385,7 +377,6 @@ async fn test_contact_name_changes() -> Result<()> {
assert_eq!(contact.get_authname(), "Foo Flobby");
assert_eq!(contact.get_name(), "Falk");
assert_eq!(contact.get_display_name(), "Falk");
assert_eq!(contact.get_name_n_addr(), "Falk (f@example.org)");
let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?;
assert_eq!(contacts.len(), 0);
let contacts = Contact::get_all(&t, 0, Some("falk")).await?;
+30 -19
View File
@@ -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;
@@ -244,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.
///
@@ -352,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());
@@ -480,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),
@@ -615,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
{
@@ -816,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;
@@ -900,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:?}"));
@@ -946,6 +954,10 @@ impl Context {
"show_emails",
self.get_config_int(Config::ShowEmails).await?.to_string(),
);
res.insert(
"who_can_call_me",
self.get_config_int(Config::WhoCanCallMe).await?.to_string(),
);
res.insert(
"download_limit",
self.get_config_int(Config::DownloadLimit)
@@ -1094,6 +1106,10 @@ impl Context {
.await?
.unwrap_or_default(),
);
res.insert(
"team_profile",
self.get_config_bool(Config::TeamProfile).await?.to_string(),
);
let elapsed = time_elapsed(&self.creation_time);
res.insert("uptime", duration_to_str(elapsed));
@@ -1331,10 +1347,5 @@ impl Context {
}
}
/// Returns core version as a string.
pub fn get_version_str() -> &'static str {
&DC_VERSION_STR
}
#[cfg(test)]
mod context_tests;
+6 -2
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)?;
+5 -5
View File
@@ -419,11 +419,11 @@ pub enum EventType {
/// One or more transports has changed.
///
/// This event is used for tests to detect when transport
/// synchronization messages arrives.
/// UIs don't need to use it, it is unlikely
/// that user modifies transports on multiple
/// devices simultaneously.
/// 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.
+55 -96
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,54 +1110,13 @@ 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<()> {
if context.get_config_bool(Config::TeamProfile).await? {
info!(context, "Team profile, skipping seen flag synchronization.");
return Ok(());
}
let rows = context
.sql
.query_map_vec(
@@ -1224,6 +1185,11 @@ impl Session {
return Ok(());
}
if context.get_config_bool(Config::TeamProfile).await? {
info!(context, "Team profile, skipping seen flag synchronization.");
return Ok(());
}
let create = false;
let folder_exists = self
.select_with_uidvalidity(context, folder, create)
@@ -1277,10 +1243,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 +1466,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 +1512,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 +1531,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 +2077,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 +2314,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 +2325,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)?;
+17 -13
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
@@ -202,10 +207,9 @@ impl MsgId {
ret += &format!("Sent: {fts}");
let from_contact = Contact::get_by_id(context, msg.from_id).await?;
let name = from_contact.get_name_n_addr();
let name = from_contact.get_name();
if let Some(override_sender_name) = msg.get_override_sender_name() {
let addr = from_contact.get_addr();
ret += &format!(" by ~{override_sender_name} ({addr})");
ret += &format!(" by ~{override_sender_name}");
} else {
ret += &format!(" by {name}");
}
@@ -253,7 +257,7 @@ impl MsgId {
let name = Contact::get_by_id(context, contact_id)
.await
.map(|contact| contact.get_name_n_addr())
.map(|contact| contact.get_display_name().to_owned())
.unwrap_or_default();
ret += &format!(" by {name}");
@@ -845,11 +849,10 @@ impl Message {
let contact = if self.from_id != ContactId::SELF {
match chat.typ {
Chattype::Group
| Chattype::OutBroadcast
| Chattype::InBroadcast
| Chattype::Mailinglist => Some(Contact::get_by_id(context, self.from_id).await?),
Chattype::Single => None,
Chattype::Group | Chattype::Mailinglist => {
Some(Contact::get_by_id(context, self.from_id).await?)
}
Chattype::Single | Chattype::OutBroadcast | Chattype::InBroadcast => None,
}
} else {
None
@@ -1712,6 +1715,7 @@ pub async fn delete_msgs_ex(
msgs: deleted_rfc724_mid,
})
.await?;
context.scheduler.interrupt_smtp().await;
}
for &msg_id in msg_ids {
+76 -9
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.
+2 -2
View File
@@ -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)?;
+37 -19
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(())
}
}
+16 -4
View File
@@ -38,7 +38,9 @@ use crate::param::{Param, Params};
use crate::peer_channels::{add_gossip_peer_from_header, insert_topic_stub};
use crate::reaction::{Reaction, set_msg_reaction};
use crate::rusqlite::OptionalExtension;
use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
use crate::securejoin::{
self, get_secure_join_step, handle_securejoin_handshake, observe_securejoin_on_other_device,
};
use crate::simplify;
use crate::stats::STATISTICS_BOT_EMAIL;
use crate::stock_str;
@@ -673,16 +675,26 @@ pub(crate) async fn receive_imf_inner(
.await?;
let received_msg;
if mime_parser.get_header(HeaderDef::SecureJoin).is_some() {
if let Some(_step) = get_secure_join_step(&mime_parser) {
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
};
+32
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.
+13 -25
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,
+42 -24
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>";
+93 -35
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}",
@@ -343,6 +343,62 @@ pub(crate) enum HandshakeMessage {
Propagate,
}
/// Step of Secure-Join protocol.
#[derive(Debug, Display, PartialEq, Eq)]
pub(crate) enum SecureJoinStep {
/// vc-request or vg-request
Request { invitenumber: String },
/// vc-auth-required or vg-auth-required
AuthRequired,
/// vc-request-with-auth or vg-request-with-auth
RequestWithAuth,
/// vc-contact-confirm
ContactConfirm,
/// vg-member-added
MemberAdded,
/// Deprecated step such as `vg-member-added-received` or `vc-contact-confirm-received`.
Deprecated,
/// Unknown step.
Unknown { step: String },
}
/// Parses message headers to find out which Secure-Join step the message represents.
///
/// Returns `None` if the message is not a Secure-Join message.
pub(crate) fn get_secure_join_step(mime_message: &MimeMessage) -> Option<SecureJoinStep> {
if let Some(invitenumber) = mime_message.get_header(HeaderDef::SecureJoinInvitenumber) {
// We do not care about presence of `Secure-Join: vc-request` or `Secure-Join: vg-request` header.
// This allows us to always treat `Secure-Join` header as protected and ignore it
// in the unencrypted part even though it is sent there for backwards compatibility.
Some(SecureJoinStep::Request {
invitenumber: invitenumber.to_string(),
})
} else if let Some(step) = mime_message.get_header(HeaderDef::SecureJoin) {
match step {
"vg-auth-required" | "vc-auth-required" => Some(SecureJoinStep::AuthRequired),
"vg-request-with-auth" | "vc-request-with-auth" => {
Some(SecureJoinStep::RequestWithAuth)
}
"vc-contact-confirm" => Some(SecureJoinStep::ContactConfirm),
"vg-member-added" => Some(SecureJoinStep::MemberAdded),
"vg-member-added-received" | "vc-contact-confirm-received" => {
Some(SecureJoinStep::Deprecated)
}
step => Some(SecureJoinStep::Unknown {
step: step.to_string(),
}),
}
} else {
None
}
}
/// Handle incoming secure-join handshake.
///
/// This function will update the securejoin state in the database as the protocol
@@ -362,9 +418,8 @@ pub(crate) async fn handle_securejoin_handshake(
if contact_id.is_special() {
return Err(Error::msg("Can not be called with special contact ID"));
}
let step = mime_message
.get_header(HeaderDef::SecureJoin)
.context("Not a Secure-Join message")?;
let step = get_secure_join_step(mime_message).context("Not a Secure-Join message")?;
info!(context, "Received secure-join message {step:?}.");
@@ -383,7 +438,7 @@ pub(crate) async fn handle_securejoin_handshake(
// will improve security (completely unrelated to the securejoin protocol)
// and is something we want to do in the future:
// https://www.rfc-editor.org/rfc/rfc9580.html#name-surreptitious-forwarding
if !matches!(step, "vg-request" | "vc-request") {
if !matches!(step, SecureJoinStep::Request { .. }) {
let mut self_found = false;
let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint();
for (addr, key) in &mime_message.gossiped_keys {
@@ -403,7 +458,7 @@ pub(crate) async fn handle_securejoin_handshake(
}
match step {
"vg-request" | "vc-request" => {
SecureJoinStep::Request { ref invitenumber } => {
/*=======================================================
==== Alice - the inviter side ====
==== Step 3 in "Setup verified contact" protocol ====
@@ -413,13 +468,6 @@ pub(crate) async fn handle_securejoin_handshake(
// it just ensures, we have Bobs key now. If we do _not_ have the key because eg. MitM has removed it,
// send_message() will fail with the error "End-to-end-encryption unavailable unexpectedly.", so, there is no additional check needed here.
// verify that the `Secure-Join-Invitenumber:`-header matches invitenumber written to the QR code
let invitenumber = match mime_message.get_header(HeaderDef::SecureJoinInvitenumber) {
Some(n) => n,
None => {
warn!(context, "Secure-join denied (invitenumber missing)");
return Ok(HandshakeMessage::Ignore);
}
};
if !token::exists(context, token::Namespace::InviteNumber, invitenumber).await? {
warn!(context, "Secure-join denied (bad invitenumber).");
return Ok(HandshakeMessage::Ignore);
@@ -436,24 +484,29 @@ pub(crate) async fn handle_securejoin_handshake(
)
.await?;
let prefix = mime_message
.get_header(HeaderDef::SecureJoin)
.and_then(|step| step.get(..2))
.unwrap_or("vc");
// Alice -> Bob
send_alice_handshake_msg(
context,
autocrypt_contact_id,
&format!("{}-auth-required", &step.get(..2).unwrap_or_default()),
&format!("{prefix}-auth-required"),
)
.await
.context("failed sending auth-required handshake message")?;
Ok(HandshakeMessage::Done)
}
"vg-auth-required" | "vc-auth-required" => {
SecureJoinStep::AuthRequired => {
/*========================================================
==== Bob - the joiner's side =====
==== Step 4 in "Setup verified contact" protocol =====
========================================================*/
bob::handle_auth_required(context, mime_message).await
}
"vg-request-with-auth" | "vc-request-with-auth" => {
SecureJoinStep::RequestWithAuth => {
/*==========================================================
==== Alice - the inviter side ====
==== Steps 5+6 in "Setup verified contact" protocol ====
@@ -564,14 +617,14 @@ pub(crate) async fn handle_securejoin_handshake(
==== Bob - the joiner's side ====
==== Step 7 in "Setup verified contact" protocol ====
=======================================================*/
"vc-contact-confirm" => {
SecureJoinStep::ContactConfirm => {
context.emit_event(EventType::SecurejoinJoinerProgress {
contact_id,
progress: JoinerProgress::Succeeded.into_u16(),
});
Ok(HandshakeMessage::Ignore)
}
"vg-member-added" => {
SecureJoinStep::MemberAdded => {
let Some(member_added) = mime_message.get_header(HeaderDef::ChatGroupMemberAdded)
else {
warn!(
@@ -594,13 +647,12 @@ pub(crate) async fn handle_securejoin_handshake(
});
Ok(HandshakeMessage::Propagate)
}
"vg-member-added-received" | "vc-contact-confirm-received" => {
SecureJoinStep::Deprecated => {
// Deprecated steps, delete them immediately.
Ok(HandshakeMessage::Done)
}
_ => {
warn!(context, "invalid step: {}", step);
SecureJoinStep::Unknown { ref step } => {
warn!(context, "Invalid SecureJoin step: {step:?}.");
Ok(HandshakeMessage::Ignore)
}
}
@@ -631,17 +683,20 @@ pub(crate) async fn observe_securejoin_on_other_device(
if contact_id.is_special() {
return Err(Error::msg("Can not be called with special contact ID"));
}
let step = mime_message
.get_header(HeaderDef::SecureJoin)
.context("Not a Secure-Join message")?;
let step = get_secure_join_step(mime_message).context("Not a Secure-Join message")?;
info!(context, "Observing secure-join message {step:?}.");
if !matches!(
step,
"vg-request-with-auth" | "vc-request-with-auth" | "vg-member-added" | "vc-contact-confirm"
) {
return Ok(HandshakeMessage::Ignore);
};
match step {
SecureJoinStep::Request { .. }
| SecureJoinStep::AuthRequired
| SecureJoinStep::Deprecated
| SecureJoinStep::Unknown { .. } => {
return Ok(HandshakeMessage::Ignore);
}
SecureJoinStep::RequestWithAuth
| SecureJoinStep::MemberAdded
| SecureJoinStep::ContactConfirm => {}
}
if !encrypted_and_signed(context, mime_message, &get_self_fingerprint(context).await?) {
warn!(
@@ -673,7 +728,10 @@ pub(crate) async fn observe_securejoin_on_other_device(
mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
if step == "vg-member-added" || step == "vc-contact-confirm" {
if matches!(
step,
SecureJoinStep::MemberAdded | SecureJoinStep::ContactConfirm
) {
let chat_type = if mime_message
.get_header(HeaderDef::ChatGroupMemberAdded)
.is_none()
@@ -696,14 +754,14 @@ pub(crate) async fn observe_securejoin_on_other_device(
inviter_progress(context, contact_id, chat_id, chat_type)?;
}
if step == "vg-request-with-auth" || step == "vc-request-with-auth" {
if matches!(step, SecureJoinStep::RequestWithAuth) {
// This actually reflects what happens on the first device (which does the secure
// join) and causes a subsequent "vg-member-added" message to create an unblocked
// verified group.
ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Not).await?;
}
if step == "vg-member-added" {
if matches!(step, SecureJoinStep::MemberAdded) {
Ok(HandshakeMessage::Propagate)
} else {
Ok(HandshakeMessage::Ignore)
+89 -39
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 {
@@ -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
@@ -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.
+162 -9
View File
@@ -6,8 +6,10 @@ use super::*;
use crate::chat::{CantSendReason, add_contact_to_chat, remove_contact_from_chat};
use crate::chatlist::Chatlist;
use crate::constants::Chattype;
use crate::constants::DC_CHAT_ID_TRASH;
use crate::key::self_fingerprint;
use crate::mimeparser::{GossipedKey, SystemMessage};
use crate::qr::Qr;
use crate::receive_imf::receive_imf;
use crate::stock_str::{self, messages_e2e_encrypted};
use crate::test_utils::{
@@ -100,6 +102,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 +256,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 +285,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 +1021,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 +1225,148 @@ 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(())
}
/// Tests that vc-request is processed even if the server
/// encrypts incoming unencrypted messages with OpenPGP.
///
/// For an example of software that encrypts incoming messages
/// with OpenPGP, see <https://lacre.io/>.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_vc_request_encrypted_at_rest() -> 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 Qr::AskVerifyContact { invitenumber, .. } = check_qr(bob, &qr).await? else {
panic!("Failed to parse QR code");
};
// Encrypted payload is this message encrypted to Alice's key:
/*
Content-Type: multipart/mixed;
boundary="1888168c99b6020b_ed9b3571859a92d7_eeabe50050fc923b"
--1888168c99b6020b_ed9b3571859a92d7_eeabe50050fc923b
Content-Type: text/plain; charset="utf-8"
Message-ID: <4f108a03-6c03-4f41-a6fb-fcd05b94c21f@localhost>
Content-Transfer-Encoding: 7bit
Secure-Join: vc-request
--1888168c99b6020b_ed9b3571859a92d7_eeabe50050fc923b--
*/
let payload = format!("To: <alice@example.org>
Subject: [...]
Date: Tue, 6 Jan 2026 08:20:47 +0000
References: <4f108a03-6c03-4f41-a6fb-fcd05b94c21f@localhost>
Chat-Version: 1.0
Autocrypt: addr=bob@example.net; prefer-encrypt=mutual; keydata=xsBNBF4wx1cBCADOwLS/xCd8iKDWUsyTfVzWby+ZGKPpamPTvdj0GFgnf0B1EBaA5//PjA
zbK5iKio6QNEmZagzJPkXPByJcAIRUm0T16tqDtCvxm+H93YEXpHi/XWOeJw9kohATSqUtsRO0pFJe
DvPiMTmQrEmHYoWDSQBfCrowZdvnMAlbJ9JjYOngcMeTxc0jxmPs5s17yFC+1OWu4fwWCyUM3wy1Jz
dKTcDWryrSkvmgFdUqJ7pJDk1HFTt+x9tvQlK3un9BXiRwv0u0zDSuI8eDH/dRLA4UL9Pq6vmJmBam
e1BPsE1PA7VzeTSJR2ooJXMT6o2AmH8PPUfRkv3OiWuh7LM5FSpHABEBAAHNETxib2JAZXhhbXBsZS
5uZXQ+wsCOBBMBCAA4BQJpXW0wFiEEzMtaqfbhFByUMWXx2xixjLz3BIcCGwMCHgAECwkIBwYVCAkK
CwIDFgIBAScCGQEACgkQ2xixjLz3BIcexwgAsOauz2xZ6QhVBQNX3Hg8OoYTn60w6b4XUOpI4UtqAT
bAmPr5VPhCUa5vzI19QCSekLZIVxUSl6C+7YF/mU8yJldKPgq1Kpr8tbnISNbUDpvuGz7NQdK0TEwx
RkPDw+ilqY7v2Rhhg0Sogi23Yrbyqvs72oorDjia6dBA7VLixpW1O9h/MfQ6WiXJcHSavoLqk2hFW1
y/AsVmArGiuT+kzRJWhTzojZLjQLHTKQii/AOQ3MhkJycyayI/igSEDWWwCcuqL8SF+PUJWGyU5PxU
2hTHDBsVItLLsqseM3WrwJRkBBdsdsR8+moHVyXJV9Gfl/4UFQCgltdEwWISgNTbAc7ATQReMMdXAQ
gAogeBLbIjaeJII3W2pxsu+SEusQkJVykbGYDtqyXV+XBZisY4GE0kTawK5mqSh+rDqquCxDgYWBRT
nGZwEKohnj2NG75pjfyVPhYMUdJt7+Ya1oiFvZlgrrOj1btjevq53yFtQolMN+X2oS8mlf9jSzIyPC
eDxJk1N1gxaAATg3ByAyB1Td0wDdFPp48ni8qzfyGZeYicvJlx74YnOaja2lnI/y+I9LsmmqiOgI8H
cbmH1od5qSnVjhcpBoTEA15YLIEkSE3C00Q5USlDS3EVg/IOu3FXnLl7v0hQ/jXyv88eycfpSfFcbM
Hot9VtJ4TIPIoSX7DQ+uU2SXJKiZNkVQARAQABwsB2BBgBCAAgBQJpXW0wAhsMFiEEzMtaqfbhFByU
MWXx2xixjLz3BIcACgkQ2xixjLz3BIcxvwf9G33yLtvekpNGeRbWkjRXSa083k/4CZpkF0Fb5led1O
hjRjw9KQdZj68qn1vVrDxygnYdHwZWy8WbwEkK+l3gyJ2v2D/Wsj+sHjQ1YMtPnx+EsRFWjKsolYfN
gJZ+IfMTasip8uJNxN5LIvI/svn43mzgeLUudddekmr/2eekhXfYz5covje3yf+u4rYDWQ78TLpJL5
bxTwiiUyV1ZIO1G2F4RNjv8wZHsGD3DasQck+OCV7jQNuyojcmq6J22lXN0clisI51mHzwkLAnI+Yh
jRj3nqwVSCxsmgfejap53JGHEL0CEy6KvepLqjFf8BiK4kwRBmI7/YWOyY2xBWdTew==
Secure-Join: vc-request
Secure-Join-Invitenumber: {invitenumber}
MIME-Version: 1.0
From: <bob@example.net>
Message-ID: <4f108a03-6c03-4f41-a6fb-fcd05b94c21f@localhost>
Content-Type: multipart/encrypted;
protocol=\"application/pgp-encrypted\";
boundary=\"1767687657/562150/131174/example.org\"
This is a MIME-encapsulated message.
--1767687657/562150/131174/example.org
Content-Type: application/pgp-encrypted
Content-Disposition: attachment
Version: 1
--1767687657/562150/131174/example.org
Content-Type: application/octet-stream
Content-Disposition: inline; filename=\"msg.asc\"
-----BEGIN PGP MESSAGE-----
wV4D5tq63hTeebASAQdAGKP3GqsznPOYBXOo8FYf5x8r0ZaJZUGkXIR+Oi32H1Mw
LdVEu0jcXmZ1fGZ7LLPtvfwllB2W9Bhiy/lCZZs+c98VYzSWaT0DHmp23WvTsBZn
0sDeAS0n2I6xPwYhUjqL8G1a9xdyvZRgHYUXhiARrXzx7G+aX8/KFjk6e2OZxiVy
PSHKh61T9wPYzPMlToPsI0GjXAzOgI9I3XQ8xPqihiUpnU6BSg+Tj26m9ZqdJe8v
aVAHxAu7aqz7eljp2H336Idgfvf1lXdA6gMA8HhHI729Ws/Mc8lcgefM9kzZWEIN
FOBYmf4JCdTghESpEJ5i+pF28ChqZ3+8lGzNmT66c2SQRTclwxFa4SS19D4mxkHq
iOMqj7LZ8R6IhnNVRD1V4mVapc7vFVZy8ViyZlmhJF0TBR/TEXlit6vtSkDKFiR9
Tb5zuFQVAwhun3i1tG3/Ii1ReflZNnteVlL1J1XnptOJ70vVUeO3qRqkbkiH8Mdm
Asvy24WKkz9jmEgdt0Hj8VUHtcOL5xmtsr0ZUCLUriylpzDh8E+0LTl2/DOgH+mI
D/gFLCzFHjI0jfXyLTGCYOPYAnvqJukLBT/X7JbCAI/62aq0K28Lp6beJhFs+bIz
gU6dGXsFMe/RpRHrIAkMAaM5xkxMDRuRJDxiUdS/X+Y8
=QrdS
-----END PGP MESSAGE-----
--1767687657/562150/131174/example.org--
");
// Alice receives vc-request, but it is encrypted
// by the server.
let received = receive_imf(alice, payload.as_bytes(), false)
.await?
.unwrap();
assert_eq!(received.chat_id, DC_CHAT_ID_TRASH);
// Test that Alice sends vc-auth-required after processing vc-request.
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vc-auth-required"
);
Ok(())
}
+1
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 {
+36 -41
View File
@@ -19,7 +19,7 @@ use crate::log::warn;
use crate::message::MsgId;
use crate::provider::get_provider_info;
use crate::sql::Sql;
use crate::tools::{Time, inc_and_check, normalize_text, time_elapsed};
use crate::tools::{Time, inc_and_check, time_elapsed};
use crate::transport::ConfiguredLoginParam;
const DBVERSION: i32 = 68;
@@ -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,
@@ -1456,52 +1458,45 @@ CREATE INDEX imap_sync_index ON imap_sync(transport_id, folder);
inc_and_check(&mut migration_version, 143)?;
if dbversion < migration_version {
let trans_fn = |t: &mut rusqlite::Transaction| {
t.execute_batch(
"
sql.execute_migration(
"
ALTER TABLE chats ADD COLUMN name_normalized TEXT;
ALTER TABLE contacts ADD COLUMN name_normalized TEXT;
",
)?;
migration_version,
)
.await?;
}
let mut stmt = t.prepare("UPDATE chats SET name_normalized=? WHERE id=?")?;
for res in t
.prepare("SELECT id, name FROM chats LIMIT 10000")?
.query_map((), |row| {
let id: u32 = row.get(0)?;
let name: String = row.get(1)?;
Ok((id, name))
})?
{
let (id, name) = res?;
if let Some(name_normalized) = normalize_text(&name) {
stmt.execute((name_normalized, id))?;
}
}
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");
let mut stmt = t.prepare("UPDATE contacts SET name_normalized=? WHERE id=?")?;
for res in t
.prepare(
"
SELECT id, IIF(name='', authname, name) FROM contacts
ORDER BY last_seen DESC LIMIT 10000
",
)?
.query_map((), |row| {
let id: u32 = row.get(0)?;
let name: String = row.get(1)?;
Ok((id, name))
})?
{
let (id, name) = res?;
if let Some(name_normalized) = normalize_text(&name) {
stmt.execute((name_normalized, id))?;
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(())
};
sql.execute_migration_transaction(trans_fn, migration_version)
.await?;
Ok(())
},
migration_version,
)
.await?;
}
let new_version = sql
+3 -3
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};
@@ -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?,
+1 -1
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);
+9 -17
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
+23
View File
@@ -2,6 +2,7 @@
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.
@@ -14,11 +15,15 @@ pub struct StorageUsage {
/// 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, 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,6 +51,10 @@ 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 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", ())
@@ -101,9 +110,23 @@ pub async fn get_storage_usage(ctx: &Context) -> Result<StorageUsage> {
)
.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())
}
+2 -3
View File
@@ -98,14 +98,13 @@ impl Summary {
let prefix = if msg.state == MessageState::OutDraft {
Some(SummaryPrefix::Draft(stock_str::draft(context).await))
} else if msg.from_id == ContactId::SELF {
if msg.is_info() || msg.viewtype == Viewtype::Call {
if msg.is_info() || msg.viewtype == Viewtype::Call || chat.typ == Chattype::OutBroadcast
{
None
} else {
Some(SummaryPrefix::Me(stock_str::self_msg(context).await))
}
} else if chat.typ == Chattype::Group
|| chat.typ == Chattype::OutBroadcast
|| chat.typ == Chattype::InBroadcast
|| chat.typ == Chattype::Mailinglist
|| chat.is_self_talk()
{
+6 -6
View File
@@ -175,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(());
@@ -208,7 +208,7 @@ impl Context {
grpid: None,
}))
.await?;
self.scheduler.interrupt_inbox().await;
self.scheduler.interrupt_smtp().await;
Ok(())
}
@@ -675,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;
@@ -722,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
@@ -760,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");
@@ -794,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);
+17 -47
View File
@@ -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;
}
+1 -1
View File
@@ -710,7 +710,7 @@ pub(crate) async fn send_sync_transports(context: &Context) -> Result<()> {
removed_transports,
})
.await?;
context.scheduler.interrupt_inbox().await;
context.scheduler.interrupt_smtp().await;
Ok(())
}
+9 -1
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,
+70 -2
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();